Last active
May 12, 2023 09:12
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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