react-native-mosquito-transport
Version:
React native javascript sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)
236 lines (200 loc) • 9.45 kB
JavaScript
import EngineApi from "../../helpers/engine_api";
import { deserializeE2E, prefixStoragePath } from "../../helpers/peripherals";
import { Scoped } from "../../helpers/variables";
import { DeviceEventEmitter, NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { awaitReachableServer, buildFetchInterface, buildFetchResult } from "../../helpers/utils";
import { awaitRefreshToken } from "../auth/accessor";
import { simplifyError } from "simplify-error";
const LINKING_ERROR =
`The package 'react-native-mosquito-transport' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo Go\n';
const RNMTModule = NativeModules.Mosquitodb || (
new Proxy({}, {
get() {
throw new Error(LINKING_ERROR);
},
})
);
const emitter = Platform.OS === 'android' ?
DeviceEventEmitter : new NativeEventEmitter(RNMTModule);
export class MTStorage {
constructor(config) {
this.builder = { ...config };
}
downloadFile(link = '', onComplete, destination, onProgress, options) {
const { awaitServer } = options || {};
let hasFinished, isPaused, hasCancelled;
const { projectUrl, extraHeaders } = this.builder;
if (destination && (typeof destination !== 'string' || !destination.trim())) {
onComplete?.({ error: 'destination_invalid', message: 'destination must be a non-empty string' });
return () => { };
}
if (destination) destination = prefixStoragePath(destination?.trim());
if (typeof link !== 'string' || !link.trim().startsWith(`${EngineApi.staticStorage(projectUrl)}/`)) {
onComplete?.({
error: 'invalid_link',
message: `link has an invalid value, expected a string that starts with "${EngineApi.staticStorage(projectUrl)}/"`
});
return () => { };
}
link = link.trim();
const processID = `${++Scoped.StorageProcessID}`;
const init = async () => {
if (awaitServer) await awaitReachableServer(projectUrl);
await awaitRefreshToken(projectUrl);
if (hasCancelled) return;
const progressListener = emitter.addListener('mt-download-progress', ({ processID: ref, receivedBtyes, expectedBytes }) => {
if (processID !== ref || hasFinished || hasCancelled) return;
onProgress?.({
receivedBtyes,
expectedBytes,
isPaused: !!isPaused,
pause: () => {
if (hasFinished || isPaused || hasCancelled) return;
RNMTModule.pauseDownload(processID);
isPaused = true;
},
resume: () => {
if (hasFinished || !isPaused || hasCancelled) return;
RNMTModule.resumeDownload(processID);
isPaused = false;
}
});
});
const resultListener = emitter.addListener('mt-download-status', ({ processID: ref, error, errorDes, result }) => {
if (processID !== ref) return;
if (result)
try {
result = JSON.parse(result);
} catch (e) { }
const path = result?.file || undefined;
if (!hasFinished && !hasCancelled)
onComplete?.(path ? undefined : (result?.simpleError || { error, message: errorDes }), path);
resultListener.remove();
progressListener.remove();
hasFinished = true;
});
RNMTModule.downloadFile({
url: link,
authToken: Scoped.AuthJWTToken[projectUrl],
...destination ? {
destination: destination.substring('file://'.length),
destinationDir: `${destination.substring('file://'.length)}`.split('/').slice(0, -1).join('/')
} : {},
processID,
urlName: link.split('/').pop(),
extraHeaders: extraHeaders || {},
});
}
init();
return () => {
if (hasFinished || hasCancelled) return;
RNMTModule.cancelDownload(processID);
hasCancelled = true;
setTimeout(() => {
onComplete?.({ error: 'download_aborted', message: 'The download process was aborted' });
}, 1);
}
}
uploadFile(file = '', destination = '', onComplete, onProgress, options) {
const { createHash, awaitServer } = options || {};
let hasFinished, hasCancelled;
const thisComplete = (...args) => {
if (hasFinished) return;
hasFinished = true;
onComplete?.(...args);
}
if (typeof file !== 'string' || !file.trim()) {
thisComplete?.({ error: 'file_path_invalid', message: 'file must be a non-empty string in uploadFile()' });
return () => { };
}
destination = destination?.trim?.();
try {
validateDestination(destination);
} catch (error) {
thisComplete?.({ error: 'destination_invalid', message: error });
return () => { };
}
const isAsset = file.startsWith('ph://') || file.startsWith('content://');
file = isAsset ? file.trim() : prefixStoragePath(file.trim());
const { projectUrl, uglify, extraHeaders } = this.builder;
const processID = `${++Scoped.StorageProcessID}`;
const init = async () => {
if (awaitServer) await awaitReachableServer(projectUrl);
await awaitRefreshToken(projectUrl);
if (hasCancelled) return;
const progressListener = emitter.addListener('mt-uploading-progress', ({ processID: ref, sentBytes, totalBytes }) => {
if (processID !== ref || hasFinished || hasCancelled) return;
onProgress?.({ sentBytes, totalBytes });
});
const resultListener = emitter.addListener('mt-uploading-status', ({ processID: ref, error, errorDes, result }) => {
if (processID !== ref) return;
if (result)
try {
result = JSON.parse(result);
} catch (_) { }
const downloadUrl = result?.downloadUrl || undefined;
thisComplete?.(downloadUrl ? undefined : (result?.simpleError || { error, message: errorDes }), downloadUrl);
resultListener.remove();
progressListener.remove();
});
const authToken = Scoped.AuthJWTToken[projectUrl];
RNMTModule.uploadFile({
url: EngineApi._uploadFile(projectUrl, uglify),
file: isAsset ? file : file.substring('file://'.length),
...authToken ? { authToken } : {},
createHash: createHash ? 'yes' : 'no',
destination,
processID,
extraHeaders: extraHeaders || {}
});
}
init();
return () => {
if (hasFinished || hasCancelled) return;
hasCancelled = true;
setTimeout(() => {
thisComplete?.({ error: 'upload_aborted', message: 'The upload process was aborted' });
}, 0);
RNMTModule.cancelUpload(processID);
}
}
deleteFile = (path) => deleteContent(this.builder, path);
deleteFolder = (path) => deleteContent(this.builder, path, true);
}
const { _deleteFile, _deleteFolder } = EngineApi;
const deleteContent = async (builder, path, isFolder) => {
const { projectUrl, uglify, extraHeaders, serverE2E_PublicKey } = builder;
try {
const [reqBuilder, [privateKey]] = await buildFetchInterface({
method: 'DELETE',
authToken: Scoped.AuthJWTToken[projectUrl],
body: { path },
extraHeaders,
serverE2E_PublicKey,
uglify
});
const data = await buildFetchResult(await fetch((isFolder ? _deleteFolder : _deleteFile)(projectUrl, uglify), reqBuilder), uglify);
const result = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
if (result.status !== 'success') throw 'operation not successful';
} catch (e) {
if (e?.simpleError) throw e.simpleError;
throw simplifyError('unexpected_error', `${e}`).simpleError;
}
}
const validateDestination = (t = '') => {
if (typeof t !== 'string' || !t.trim()) throw 'path must be a non-empty string';
if (t.startsWith(' ') || t.endsWith(' ')) throw 'path must be trimmed';
if (t.startsWith('./') || t.startsWith('../')) throw 'path must be absolute';
if (t.endsWith('/')) throw 'path must not end with "/"';
if ('?'.split('').some(v => t.includes(v)))
throw `path must not contain ?`;
t = t.trim();
let l = '';
t.split('').forEach(e => {
if (e === '/' && l === '/') throw 'invalid destination path, "/" cannot be duplicated side by side';
l = e;
});
};