Skip to content

Instantly share code, notes, and snippets.

@spartanatreyu
Last active May 12, 2023 09:12
Show Gist options
  • Save spartanatreyu/6ba9dd416b9a9a5b3ccd6026ecfd1de1 to your computer and use it in GitHub Desktop.
Save spartanatreyu/6ba9dd416b9a9a5b3ccd6026ecfd1de1 to your computer and use it in GitHub Desktop.
An example of using Capacitor v3's file APIs. This is using @capacitor/core: 3.0.1, and @capacitor/filesystem: 1.0.1.
import { Capacitor } from '@capacitor/core';
import { Filesystem, Directory, Encoding, WriteFileResult } from '@capacitor/filesystem';
// Basic file I/O functions
export const writeFile = async (path: string = 'secrets/text.txt', dataToWrite: string = 'hello world', isBinary: boolean = true) => {
return Filesystem.writeFile({
path: path,
data: dataToWrite,
directory: Directory.Data,
encoding: isBinary ? undefined : Encoding.UTF8, // When isBinary is set to false, file is stored as text
recursive: true
});
};
export const appendFile = async (path: string = 'secrets/text.txt', dataToAppend: string = 'hello world', isBinary: boolean = true) => {
return Filesystem.appendFile({
path: path,
data: dataToAppend,
directory: Directory.Data,
encoding: isBinary ? undefined : Encoding.UTF8 // When isBinary is set to false, file is stored as text
});
}
export const readFile = async (path: string = 'secrets/text.txt', isBinary: boolean = true) => {
return Filesystem.readFile({
path: path,
directory: Directory.Data,
encoding: isBinary ? undefined : Encoding.UTF8 // When isBinary is set to false, file is stored as text
});
};
export const deleteFile = async (path: string) => {
await Filesystem.deleteFile({
path: path,
directory: Directory.Data,
});
};
export const removeFolder = async (path: string, deleteFolderContents: boolean = false) => {
return Filesystem.rmdir({
path,
directory: Directory.Data,
recursive: deleteFolderContents
});
};
// Advanced functions
const fetchAndSaveChunk = async (localFileName: string, onlineFileURL: string, chunkNumber: number, chunkWidth: number) => {
const cacheBustedFileName = onlineFileURL + '?v=' + (new Date()).valueOf(); //to prevent ios from caching network requests
const chunkBegin = chunkNumber * chunkWidth;
const chunkEnd = chunkBegin + chunkWidth;
return fetch(cacheBustedFileName, {
method: 'GET',
headers: {'Range': `bytes=${chunkBegin}-${chunkEnd - 1}`} // -1 otherwise the last byte of this chunk is repeated in the first byte of the next chunk
})
.then(response => response.blob()) // wrap file's binary representation in a blob (JS's representation of binary)
.then(blob => new Promise((resolve, reject) => {
const blobReader = new FileReader();
blobReader.onload = function(){
const saveChunk = chunkNumber === 0 ? writeFile : appendFile;
// console.log(`fetchAndSaveChunk - ${chunkNumber === 0 ? 'saved' : 'appended'} chunk ${chunkNumber} started`);
saveChunk(localFileName, (this.result as string), true)
.then((result: WriteFileResult | void) => {
resolve(result);
// console.log(`fetchAndSaveChunk - ${chunkNumber === 0 ? 'saved' : 'appended'} chunk ${chunkNumber} finished`);
})
.catch(error => {
console.error(`caught on writeFile - ${chunkNumber === 0 ? 'saved' : 'appended'} chunk ${chunkNumber}:`);
console.error(error);
reject(error);
});
}
blobReader.readAsDataURL(blob);
})
);
};
export type OnSaveAssetProgressCallback = (bytesDownloaded: number, totalBytesToDownload: number) => void;
export const saveAsset = async (localFileName: string, onlineFileURL: string, totalFileSizeInBytes: number, onSaveAssetProgress?: OnSaveAssetProgressCallback) => {
// Figure out file chunk size
//
// If we don't split files into chunks, we can quickly fill up the device's available ram. This is a major issue on iOS devices because
// if the garbage collection passes are too infrequent and the ram fills up, the device will force crash the app while it is still
// downloading the assets. But, if we break the files into chunks and reuse the same references, we make it easier for the garbage
// collector to clean up those parts of the assets that aren't required any more.
// const chunkSize = (2 * (1024 * 1024)) - 2; // 2MB minus two bytes.
const chunkSize = (10 * (1024 * 1024)) - 1; // 10MB minus one byte.
// chunkSize short explanation:
// The minus at the end is to ensure that each chunk except the last is divisible by 3 bytes.
//
// chunkSize long explanation:
// This is useful when debugging as the non-device browser can polyfill the file system api to save chunks to the browser's Indexed DB.
// However each chunk is saved as a base64 encoded string. Base64 encodes data in 3 byte chunks, padded up to 3 bytes if required. This
// means that any file can be split apart, encoded, concatenated then decoded successfully so long as each chunk's number of bytes is
// divisible by 3.
// Figure out how many chunks to be downloaded
const totalNumberOfChunks = Math.ceil(totalFileSizeInBytes / chunkSize); // A 3.2MB file would result in 4x 1MB chunks needing to be downloaded
// Loop over every required chunk and save it
for (let chunkNumber = 0; chunkNumber < totalNumberOfChunks; chunkNumber++){
await fetchAndSaveChunk(localFileName, onlineFileURL, chunkNumber, chunkSize);
if (onSaveAssetProgress !== undefined){
// Math.min so we don't over-report the number of bytes downloaded
onSaveAssetProgress(Math.min( ( chunkNumber + 1 ) * chunkSize, totalFileSizeInBytes ), totalFileSizeInBytes);
}
}
return true;
};
// Making some simplified platform agnostic file system types here since capacitor 3's file system plugin's types
// aren't that strongly defined, and I want something that could work on non-capacitor based platforms too.
type FSDirectory = {
items: FSDirectoryItems
type: 'directory'
};
type FSFile = {
size: number
type: 'file'
};
type FSItem = {simpleURI: string, nativeURI: string} & (FSDirectory | FSFile);
export type FSDirectoryItems = FSItem[];
/**
* Scans the file system available to the app and generates a representation of it.
*
* Useful for finding files.
*
* @param {string} path - The base directory that the file system scan should start from. This parameter is used by this function recursively to search inside directories.
*
* @returns {Promise<FSDirectoryItems>} - The representation of the file system
*/
export const readFileSystem = async (path: string = ''): Promise<FSDirectoryItems> => {
const results: FSDirectoryItems = [];
const fs = await Filesystem.readdir({
path,
directory: Directory.Data
});
for (const fsEntry of fs.files) {
const simpleURI = path === '' ? fsEntry : `${path}/${fsEntry}`;
const fsEntryMetadata = await Filesystem.stat({
path: simpleURI,
directory: Directory.Data
});
const nativeURI = fsEntryMetadata.uri;
// const nativeURIBasePath = nativeURI.slice(0, nativeURI.length - simplePath.length);
if (fsEntryMetadata.type === 'NSFileTypeRegular' || fsEntryMetadata.type === 'file'){
results.push({
simpleURI,
nativeURI,
type: 'file',
size: fsEntryMetadata.size
});
}
else if (fsEntryMetadata.type === 'NSFileTypeDirectory' || fsEntryMetadata.type === 'directory'){
results.push({
simpleURI,
nativeURI,
type: 'directory',
// call self recursively, so end result is an object that contains all files and folders (including nested items inside folders)
items: await readFileSystem(simpleURI)
});
}
else {
console.log('encountered a file system entry that is not a file or folder:');
console.log({fsEntry: fsEntryMetadata});
}
}
return results;
};
export const getSrcURLFromFS = async (simpleURI: string, fs: FSDirectoryItems): Promise<string> => {
// Search through all files for specified file
for (const fileItem of fs){
// If we have found the file
if (fileItem.simpleURI === simpleURI){
// Convert file's native URI to a src that can be read from inside the webview's content
return Capacitor.convertFileSrc(fileItem.nativeURI);
}
// If the current file is a directory, search inside recursively
if (fileItem.type === 'directory'){
const recursiveResult = await getSrcURLFromFS(simpleURI, fileItem.items);
// If we have found the file
if (recursiveResult !== ''){
return recursiveResult; // return the already converted result
}
}
}
// File not found, return nothing
return '';
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment