react-native-cloud-storage
Version:
☁️ Save to & read from iCloud and Google Drive using React Native
213 lines (209 loc) • 6.75 kB
JavaScript
;
import { MimeTypes } from './types';
import { localFileSystem } from '../../utils/local-fs';
const BASE_URL = 'https://www.googleapis.com/drive/v3';
const BASE_UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v3';
const MULTIPART_BOUNDARY = 'foo_bar_baz';
export class GoogleDriveHttpError extends Error {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(message, status, json) {
super(message);
this.status = status;
this.json = json;
}
}
// TODO: properly handle errors
export default class GoogleDriveApiClient {
_fetchTimeout = null;
constructor(options) {
this.options = options;
}
buildQueryString(query) {
let result = Object.entries(query).filter(([, value]) => value !== undefined).map(([key, value]) => {
if (typeof value === 'boolean') return `${encodeURIComponent(key)}=${value ? 'true' : 'false'}`;
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}).join('&');
if (result) {
result = `?${result}`;
}
return result;
}
async request(operation, {
queryParameters,
baseUrl,
...options
} = {}) {
const {
timeout,
accessToken
} = this.options;
let path = `${baseUrl ?? BASE_URL}${operation}`;
if (queryParameters) {
path += this.buildQueryString(queryParameters);
}
if (this._fetchTimeout !== null) clearTimeout(this._fetchTimeout);
const abortController = new AbortController();
this._fetchTimeout = setTimeout(() => {
abortController.abort();
}, timeout);
const response = await fetch(path, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`
},
signal: abortController.signal
});
if (!response.ok) {
let errorMessage;
let json = null;
try {
json = await response.json();
errorMessage = typeof json === 'object' && json !== null && 'error' in json && typeof json.error === 'object' && json.error !== null && 'message' in json.error && typeof json.error.message === 'string' ? json.error.message : `Request failed with status ${response.status}`;
} catch {
errorMessage = `Request failed with status ${response.status}`;
}
throw new GoogleDriveHttpError(errorMessage, response.status, json);
}
if (response.status === 204) {
return undefined;
}
if (options?.headers && 'Accept' in options.headers && options.headers.Accept !== 'application/json') {
return response.text();
}
return response.json();
}
buildMultiPartBody(metadata, media) {
const body = [];
body.push(`--${MULTIPART_BOUNDARY}\r\n`, `Content-Type: ${MimeTypes.JSON}; charset=UTF-8\r\n\r\n`, `${JSON.stringify(metadata)}\r\n`, `--${MULTIPART_BOUNDARY}\r\n`, `Content-Type: ${media.mimeType}\r\n\r\n`, `${media.body}\r\n`, `--${MULTIPART_BOUNDARY}--`);
return body.join('');
}
async listFiles(space, query) {
const files = [];
let pageToken;
const fields = ['id', 'kind', 'mimeType', 'name', 'parents', 'spaces', 'size', 'createdTime', 'modifiedTime'];
do {
const queryParameters = {
fields: `files(${fields.join(',')}),nextPageToken`,
spaces: space,
pageToken,
q: query
};
const response = await this.request(`/files`, {
queryParameters
});
files.push(...response.files);
pageToken = response.nextPageToken ?? undefined;
} while (pageToken);
return files;
}
async getFile(fileId) {
const queryParameters = {
fields: ['id', 'kind', 'mimeType', 'name', 'parents', 'spaces', 'size', 'createdTime', 'modifiedTime'].join(',')
};
return this.request(`/files/${fileId}`, {
queryParameters
});
}
async getFileText(fileId) {
return this.request(`/files/${fileId}`, {
queryParameters: {
alt: 'media'
},
headers: {
Accept: 'text/plain'
}
});
}
async deleteFile(fileId) {
return this.request(`/files/${fileId}`, {
method: 'DELETE'
});
}
async createFile(metadata, media) {
if ('body' in media) {
const multipartRequestBody = this.buildMultiPartBody(metadata, media);
await this.request(`/files`, {
queryParameters: {
uploadType: 'multipart'
},
method: 'POST',
headers: {
'Content-Type': `multipart/related; boundary=${MULTIPART_BOUNDARY}`,
'Content-Length': multipartRequestBody.length.toString()
},
body: multipartRequestBody,
baseUrl: BASE_UPLOAD_URL
});
} else {
// First, create an empty file with the required metadata
const file = await this.request(`/files`, {
method: 'POST',
headers: {
'Content-Type': MimeTypes.JSON
},
body: JSON.stringify({
...metadata,
mimeType: media.mimeType
})
});
// Then, upload the binary content via the native helper module
const remoteUri = `${BASE_UPLOAD_URL}/files/${file.id}?uploadType=media`;
await localFileSystem.uploadFile(media.localPath, remoteUri, {
method: 'PATCH',
uploadType: 'binary',
headers: {
'Authorization': `Bearer ${this.options.accessToken}`,
'Content-Type': media.mimeType
}
});
}
}
async createDirectory(metadata) {
await this.request(`/files`, {
method: 'POST',
headers: {
'Content-Type': `application/json`
},
body: JSON.stringify({
...metadata,
mimeType: MimeTypes.FOLDER
})
});
}
async updateFile(fileId, media) {
if ('body' in media) {
await this.request(`/files/${fileId}`, {
queryParameters: {
uploadType: 'media'
},
method: 'PATCH',
headers: {
'Content-Type': media.mimeType,
'Content-Length': media.body.length.toString()
},
body: media.body,
baseUrl: BASE_UPLOAD_URL
});
} else {
const remoteUri = `${BASE_UPLOAD_URL}/files/${fileId}?uploadType=media`;
await localFileSystem.uploadFile(media.localPath, remoteUri, {
method: 'PATCH',
uploadType: 'binary',
headers: {
'Authorization': `Bearer ${this.options.accessToken}`,
'Content-Type': media.mimeType
}
});
}
}
async downloadFile(fileId, localPath) {
const remoteUri = `${BASE_URL}/files/${fileId}?alt=media`;
await localFileSystem.downloadFile(remoteUri, localPath, {
headers: {
Authorization: `Bearer ${this.options.accessToken}`
}
});
}
}
//# sourceMappingURL=client.js.map