UNPKG

react-native-cloud-storage

Version:

☁️ Save to & read from iCloud and Google Drive using React Native

377 lines (374 loc) 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _native = require("../../types/native"); var _cloudStorageError = _interopRequireDefault(require("../../utils/cloud-storage-error")); var _types = require("./types"); var _client = _interopRequireWildcard(require("./client")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * A JavaScript-based implementation of the Google Drive API that implements the cloud storage interface. */ class GoogleDrive { constructor(options) { this.options = options; this.drive = new _client.default(options); return new Proxy(this, { // before calling any function, check if the access token is set get(target, property) { const allowedFunctions = ['isCloudAvailable']; if (typeof target[property] === 'function' && !allowedFunctions.includes(property.toString())) { const { accessToken } = options; if (!accessToken?.length) { throw new _cloudStorageError.default(`Google Drive access token is not set, cannot call function ${property.toString()}`, _native.NativeCloudStorageErrorCode.ACCESS_TOKEN_MISSING); } } return target[property]; } }); } isCloudAvailable = async () => { const { accessToken } = this.options; return !!accessToken?.length; }; getRootDirectory(scope) { switch (scope) { case 'documents': { return 'drive'; } case 'app_data': { return 'appDataFolder'; } } } resolvePathToDirectories(path) { if (path.startsWith('/')) path = path.slice(1); if (path.endsWith('/')) path = path.slice(0, -1); const directories = path.split('/'); const actualFilename = directories.pop() ?? ''; return { directories, filename: actualFilename }; } findParentDirectoryId(files, directoryTree) { const possibleTopDirectories = files.filter(f => f.mimeType === _types.MimeTypes.FOLDER).filter(f => f.name === directoryTree[0]); let topDirectoryId; if (possibleTopDirectories.length === 0) return null;else if (possibleTopDirectories.length === 1) { topDirectoryId = possibleTopDirectories[0].id; } else { /* when multiple directories carry the same name, we need to check every one of them if their parent id exists in the files array - if it does not, it means that the directory is a child of the root directory and the one we're looking for */ for (const possibleTopDirectory of possibleTopDirectories) { if (!files.some(f => f.id === possibleTopDirectory.parents[0] && f.mimeType === _types.MimeTypes.FOLDER)) { topDirectoryId = possibleTopDirectory.id; break; } } } if (!topDirectoryId) { throw new _cloudStorageError.default(`Could not find top directory with name ${directoryTree[0]}`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND); } // now, we traverse the directories array and get the id of the last directory from the files array let currentDirectoryId = topDirectoryId; for (let index = 1; index < directoryTree.length; index++) { const currentDirectory = files.find(f => f.id === currentDirectoryId); if (!currentDirectory) throw new _cloudStorageError.default(`Could not find directory with id ${currentDirectoryId}`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND); const nextDirectory = files.find(f => f.name === directoryTree[index] && f.parents[0] === currentDirectoryId); if (!nextDirectory) throw new _cloudStorageError.default(`Could not find directory with name ${directoryTree[index]}`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND); currentDirectoryId = nextDirectory.id; } return currentDirectoryId; } /** * Gets the Google Drive ID of the root directory for the given scope. * @param scope The scope to get the root directory for. * @returns A promise that resolves to the ID of the root directory or null if it could not be found. */ async getRootDirectoryId(scope) { const files = await this.drive.listFiles(this.getRootDirectory(scope)); for (const file of files) { if (!files.some(f => f.id === file.parents[0])) return file.parents[0] ?? null; } return null; } checkIfMultipleFilesWithSameName(path, files, filename, parentDirectoryId) { const { strictFilenames } = this.options; const possibleFiles = parentDirectoryId ? files.filter(f => f.name === filename && f.parents[0] === parentDirectoryId) : files.filter(f => f.name === filename && !files.some(f2 => f2.id === f.parents[0])); if (possibleFiles.length <= 1) return; if (strictFilenames) { throw new _cloudStorageError.default(`Multiple files with the same name found at path ${path}: ${possibleFiles.map(f => f.id).join(', ')}`, _native.NativeCloudStorageErrorCode.MULTIPLE_FILES_SAME_NAME); } } async getFileId(path, scope, throwIf = false) { try { const files = await this.drive.listFiles(this.getRootDirectory(scope)); if (path === '' || path === '/') { const rootDirectoryId = await this.getRootDirectoryId(scope); if (!rootDirectoryId) throw new _cloudStorageError.default(`Root directory in scope ${scope} not found`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND); return rootDirectoryId; } const { directories, filename } = this.resolvePathToDirectories(path); const parentDirectoryId = this.findParentDirectoryId(files, directories); let file; if (parentDirectoryId === null) { this.checkIfMultipleFilesWithSameName(path, files, filename, null); /* when the file is supposed to be in the root directory, we need to get the file where the name is the filename and the first parent has an id which does not exist in the files array */ file = files.find(f => f.name === filename && !files.some(f2 => f2.id === f.parents[0])); } else { this.checkIfMultipleFilesWithSameName(path, files, filename, parentDirectoryId); file = files.find(f => f.name === filename && f.parents[0] === parentDirectoryId); } if (!file) throw new _cloudStorageError.default(`File not found`, _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND); if (file.mimeType === _types.MimeTypes.FOLDER && throwIf === 'directory') { throw new _cloudStorageError.default(`Path ${path} is a directory`, _native.NativeCloudStorageErrorCode.PATH_IS_DIRECTORY); } else if (file.mimeType !== _types.MimeTypes.FOLDER && throwIf === 'file') { throw new _cloudStorageError.default(`Path ${path} is a file`, _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND); } return file.id; } catch (error) { if (error instanceof _client.GoogleDriveHttpError && error.json?.error?.status === 'UNAUTHENTICATED') { throw new _cloudStorageError.default(`Could not authenticate with Google Drive`, _native.NativeCloudStorageErrorCode.AUTHENTICATION_FAILED, error.json); } else { if (error instanceof _cloudStorageError.default) throw error; throw new _cloudStorageError.default(`Could not get file id for path ${path}`, _native.NativeCloudStorageErrorCode.UNKNOWN, error); } } } async fileExists(path, scope) { try { await this.getFileId(path, scope); return true; } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) return false;else throw error; } } async appendToFile(path, data, scope) { let fileId; let previousContent = ''; try { fileId = await this.getFileId(path, scope); previousContent = await this.drive.getFileText(fileId); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* do nothing, simply create the file */ } else { throw error; } } if (fileId) { await this.drive.updateFile(fileId, { body: previousContent + data, mimeType: _types.MimeTypes.TEXT }); } else { const files = await this.drive.listFiles(this.getRootDirectory(scope)); const { directories, filename } = this.resolvePathToDirectories(path); const parentDirectoryId = this.findParentDirectoryId(files, directories); await this.drive.createFile({ name: filename, parents: parentDirectoryId ? [parentDirectoryId] : scope === 'app_data' ? [this.getRootDirectory(scope)] : undefined }, { body: data, mimeType: _types.MimeTypes.TEXT }); } } async createFile(path, data, scope, overwrite) { let fileId; if (overwrite) { try { fileId = await this.getFileId(path, scope); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* do nothing, simply create the file */ } else { throw error; } } } else { try { await this.getFileId(path, scope); throw new _cloudStorageError.default(`File ${path} already exists`, _native.NativeCloudStorageErrorCode.FILE_ALREADY_EXISTS); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* do nothing, simply create the file */ } else { throw error; } } } if (fileId) { await this.drive.updateFile(fileId, { body: data, mimeType: _types.MimeTypes.TEXT }); } else { const files = await this.drive.listFiles(this.getRootDirectory(scope)); const { directories, filename } = this.resolvePathToDirectories(path); const parentDirectoryId = this.findParentDirectoryId(files, directories); await this.drive.createFile({ name: filename, parents: parentDirectoryId ? [parentDirectoryId] : scope === 'app_data' ? [this.getRootDirectory(scope)] : undefined }, { body: data, mimeType: _types.MimeTypes.TEXT }); } } async listFiles(path, scope) { const allFiles = await this.drive.listFiles(this.getRootDirectory(scope)); if (path === '') { const rootDirectoryId = await this.getRootDirectoryId(scope); return [...new Set(allFiles.filter(f => (f.parents ?? [])[0] === rootDirectoryId).map(f => f.name))]; } else { const fileId = await this.getFileId(path, scope); const files = allFiles.filter(f => (f.parents ?? [])[0] === fileId); return [...new Set(files.map(f => f.name))]; } } async createDirectory(path, scope) { try { await this.getFileId(path, scope); throw new _cloudStorageError.default(`File ${path} already exists`, _native.NativeCloudStorageErrorCode.FILE_ALREADY_EXISTS); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* do nothing, simply create the file */ } else if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.PATH_IS_DIRECTORY) { throw new _cloudStorageError.default(`Directory ${path} already exists`, _native.NativeCloudStorageErrorCode.FILE_ALREADY_EXISTS); } else { throw error; } } const files = await this.drive.listFiles(this.getRootDirectory(scope)); const { directories, filename } = this.resolvePathToDirectories(path); const parentDirectoryId = this.findParentDirectoryId(files, directories); await this.drive.createDirectory({ name: filename, parents: parentDirectoryId ? [parentDirectoryId] : scope === 'app_data' ? [this.getRootDirectory(scope)] : undefined }); } async readFile(path, scope) { const fileId = await this.getFileId(path, scope); const content = await this.drive.getFileText(fileId); return content; } async triggerSync(_path, _scope) { // Triggering file synchronization in Google Drive is not necessary / possible, as they need to be downloaded on every read operation via the API anyway return; } async deleteFile(path, scope) { // if trying to pass a directory, throw an error const fileId = await this.getFileId(path, scope, 'directory'); await this.drive.deleteFile(fileId); } async deleteDirectory(path, recursive, scope) { // if trying to pass a file, throw an error const fileId = await this.getFileId(path, scope, 'file'); if (!recursive) { // check if the directory is empty const files = await this.drive.listFiles(this.getRootDirectory(scope)); const filesInDirectory = files.filter(f => (f.parents ?? [])[0] === fileId); if (filesInDirectory.length > 0) { throw new _cloudStorageError.default(`Directory ${path} is not empty`, _native.NativeCloudStorageErrorCode.DELETE_ERROR, filesInDirectory); } } await this.drive.deleteFile(fileId); } async statFile(path, scope) { const fileId = await this.getFileId(path, scope, false); const file = await this.drive.getFile(fileId); return { size: file.size ?? 0, birthtimeMs: new Date(file.createdTime).getTime(), mtimeMs: new Date(file.modifiedTime).getTime(), isDirectory: file.mimeType === _types.MimeTypes.FOLDER, isFile: file.mimeType !== _types.MimeTypes.FOLDER }; } async downloadFile(remotePath, localPath, scope) { const fileId = await this.getFileId(remotePath, scope, 'directory'); try { await this.drive.downloadFile(fileId, localPath); } catch (error) { if (error instanceof _cloudStorageError.default) throw error; throw new _cloudStorageError.default(`Could not download file ${remotePath} to ${localPath}`, _native.NativeCloudStorageErrorCode.UNKNOWN, error); } } async uploadFile(remotePath, localPath, mimeType, scope, overwrite) { let fileId; if (overwrite) { try { fileId = await this.getFileId(remotePath, scope); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* File doesn't exist -> we'll create it below */ } else { throw error; } } } else { try { await this.getFileId(remotePath, scope); throw new _cloudStorageError.default(`File ${remotePath} already exists`, _native.NativeCloudStorageErrorCode.FILE_ALREADY_EXISTS); } catch (error) { if (error instanceof _cloudStorageError.default && error.code === _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND) { /* not found -> ok, we'll create */ } else if (error instanceof _cloudStorageError.default) { throw error; } else { throw error; } } } if (fileId) { // Overwrite existing file await this.drive.updateFile(fileId, { mimeType, localPath }); } else { // Need to create a new file first const files = await this.drive.listFiles(this.getRootDirectory(scope)); const { directories, filename } = this.resolvePathToDirectories(remotePath); const parentDirectoryId = this.findParentDirectoryId(files, directories); await this.drive.createFile({ name: filename, parents: parentDirectoryId ? [parentDirectoryId] : scope === 'app_data' ? [this.getRootDirectory(scope)] : undefined }, { mimeType, localPath }); } } } exports.default = GoogleDrive; //# sourceMappingURL=index.js.map