react-native-cloud-storage
Version:
☁️ Save to & read from iCloud and Google Drive using React Native
399 lines (397 loc) • 16.2 kB
JavaScript
;
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 'documents_legacy':
{
return 'drive';
}
case 'app_data':
{
return 'appDataFolder';
}
}
}
isRootPath(path) {
return path === '' || path === '/';
}
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
};
}
escapeDriveQueryValue(value) {
return value.replaceAll('\\', String.raw`\\`).replaceAll("'", String.raw`\'`);
}
async getQueryRootParentId(scope) {
if (scope === 'app_data') {
return this.getRootDirectory(scope);
}
return this.getRootDirectoryId(scope);
}
async findDirectoryByNameAndParent(name, parentId, scope) {
const escapedName = this.escapeDriveQueryValue(name);
const escapedParentId = this.escapeDriveQueryValue(parentId);
const query = `name = '${escapedName}' and '${escapedParentId}' in parents and mimeType = '${_types.MimeTypes.FOLDER}' and trashed = false`;
return this.drive.listFiles(this.getRootDirectory(scope), query);
}
async findFilesByNameAndParent(name, parentId, scope) {
const escapedName = this.escapeDriveQueryValue(name);
const escapedParentId = this.escapeDriveQueryValue(parentId);
const query = `name = '${escapedName}' and '${escapedParentId}' in parents and trashed = false`;
return this.drive.listFiles(this.getRootDirectory(scope), query);
}
async findFilesByParent(parentId, scope) {
const escapedParentId = this.escapeDriveQueryValue(parentId);
const query = `'${escapedParentId}' in parents and trashed = false`;
return this.drive.listFiles(this.getRootDirectory(scope), query);
}
async findParentDirectoryId(directoryTree, scope) {
let parentDirectoryId = await this.getQueryRootParentId(scope);
for (const directoryName of directoryTree) {
const directories = await this.findDirectoryByNameAndParent(directoryName, parentDirectoryId, scope);
if (directories.length === 0) {
return null;
}
parentDirectoryId = directories[0].id;
}
return parentDirectoryId;
}
/**
* 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.
*/
async getRootDirectoryId(scope) {
if (scope !== 'app_data') {
return 'root';
}
const files = await this.drive.listFiles(this.getRootDirectory(scope));
for (const file of files) {
const parentId = file.parents?.[0];
if (parentId && !files.some(candidate => candidate.id === parentId)) {
return parentId;
}
}
return this.getRootDirectory(scope);
}
checkIfMultipleFilesWithSameName(path, files) {
const {
strictFilenames
} = this.options;
if (files.length <= 1) return;
if (strictFilenames) {
throw new _cloudStorageError.default(`Multiple files with the same name found at path ${path}: ${files.map(f => f.id).join(', ')}`, _native.NativeCloudStorageErrorCode.MULTIPLE_FILES_SAME_NAME);
}
}
async getFileId(path, scope, throwIf = false) {
try {
if (this.isRootPath(path)) {
if (throwIf === 'directory') {
throw new _cloudStorageError.default(`Path ${path} is a directory`, _native.NativeCloudStorageErrorCode.PATH_IS_DIRECTORY);
}
const rootDirectoryId = await this.getRootDirectoryId(scope);
if (scope !== 'app_data') {
await this.drive.getFile(rootDirectoryId);
}
return rootDirectoryId;
}
const {
directories,
filename
} = this.resolvePathToDirectories(path);
const parentDirectoryId = await this.findParentDirectoryId(directories, scope);
if (parentDirectoryId === null) {
throw new _cloudStorageError.default(`File not found`, _native.NativeCloudStorageErrorCode.FILE_NOT_FOUND);
}
const files = await this.findFilesByNameAndParent(filename, parentDirectoryId, scope);
this.checkIfMultipleFilesWithSameName(path, files);
const file = files[0];
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, 'directory');
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 {
directories,
filename
} = this.resolvePathToDirectories(path);
const parentDirectoryId = await this.findParentDirectoryId(directories, scope);
if (parentDirectoryId === null) {
throw new _cloudStorageError.default(`Directory not found`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND);
}
await this.drive.createFile({
name: filename,
parents: [parentDirectoryId]
}, {
body: data,
mimeType: _types.MimeTypes.TEXT
});
}
}
async createFile(path, data, scope, overwrite) {
let fileId;
if (overwrite) {
try {
fileId = await this.getFileId(path, scope, 'directory');
} 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, 'directory');
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 {
directories,
filename
} = this.resolvePathToDirectories(path);
const parentDirectoryId = await this.findParentDirectoryId(directories, scope);
if (parentDirectoryId === null) {
throw new _cloudStorageError.default(`Directory not found`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND);
}
await this.drive.createFile({
name: filename,
parents: [parentDirectoryId]
}, {
body: data,
mimeType: _types.MimeTypes.TEXT
});
}
}
async listFiles(path, scope) {
const parentDirectoryId = this.isRootPath(path) ? await this.getQueryRootParentId(scope) : await this.getFileId(path, scope);
const files = await this.findFilesByParent(parentDirectoryId, scope);
return [...new Set(files.map(f => f.name))];
}
async createDirectory(path, scope) {
if (this.isRootPath(path)) {
throw new _cloudStorageError.default(`Directory ${path} already exists`, _native.NativeCloudStorageErrorCode.FILE_ALREADY_EXISTS);
}
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 directory */
} 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 {
directories,
filename
} = this.resolvePathToDirectories(path);
const parentDirectoryId = await this.findParentDirectoryId(directories, scope);
if (parentDirectoryId === null) {
throw new _cloudStorageError.default(`Directory not found`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND);
}
await this.drive.createDirectory({
name: filename,
parents: [parentDirectoryId]
});
}
async readFile(path, scope) {
const fileId = await this.getFileId(path, scope, 'directory');
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 filesInDirectory = await this.findFilesByParent(fileId, scope);
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, 'directory');
} 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, 'directory');
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 {
directories,
filename
} = this.resolvePathToDirectories(remotePath);
const parentDirectoryId = await this.findParentDirectoryId(directories, scope);
if (parentDirectoryId === null) {
throw new _cloudStorageError.default(`Directory not found`, _native.NativeCloudStorageErrorCode.DIRECTORY_NOT_FOUND);
}
await this.drive.createFile({
name: filename,
parents: [parentDirectoryId]
}, {
mimeType,
localPath
});
}
}
}
exports.default = GoogleDrive;
//# sourceMappingURL=index.js.map