Merge pull request #34 from ente-io/lfu-with-multipart
Large file upload
This commit is contained in:
commit
4fc18bf91d
|
@ -36,6 +36,7 @@
|
||||||
"react-window-infinite-loader": "^1.0.5",
|
"react-window-infinite-loader": "^1.0.5",
|
||||||
"scrypt-js": "^3.0.1",
|
"scrypt-js": "^3.0.1",
|
||||||
"styled-components": "^5.2.0",
|
"styled-components": "^5.2.0",
|
||||||
|
"xml-js": "^1.6.11",
|
||||||
"yup": "^0.29.3"
|
"yup": "^0.29.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -53,13 +53,17 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFile = async (file: file) => {
|
getFile = async (file: file) => {
|
||||||
if (!this.fileDownloads.get(file.id)) {
|
try {
|
||||||
const download = (async () => {
|
if (!this.fileDownloads.get(file.id)) {
|
||||||
return await this.downloadFile(file);
|
const download = (async () => {
|
||||||
})();
|
return await this.downloadFile(file);
|
||||||
this.fileDownloads.set(file.id, download);
|
})();
|
||||||
|
this.fileDownloads.set(file.id, download);
|
||||||
|
}
|
||||||
|
return await this.fileDownloads.get(file.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get File', e);
|
||||||
}
|
}
|
||||||
return await this.fileDownloads.get(file.id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private async downloadFile(file: file) {
|
private async downloadFile(file: file) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import HTTPService from './HTTPService';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
|
|
||||||
import { collection } from './collectionService';
|
import { collection } from './collectionService';
|
||||||
import { MetadataObject } from './uploadService';
|
import { DataStream, MetadataObject } from './uploadService';
|
||||||
import CryptoWorker from 'utils/crypto/cryptoWorker';
|
import CryptoWorker from 'utils/crypto/cryptoWorker';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
|
@ -12,7 +12,7 @@ const DIFF_LIMIT: number = 2500;
|
||||||
const FILES = 'files';
|
const FILES = 'files';
|
||||||
|
|
||||||
export interface fileAttribute {
|
export interface fileAttribute {
|
||||||
encryptedData?: Uint8Array;
|
encryptedData?: DataStream | Uint8Array;
|
||||||
objectKey?: string;
|
objectKey?: string;
|
||||||
decryptionHeader: string;
|
decryptionHeader: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,12 @@ import { FILE_TYPE } from 'pages/gallery';
|
||||||
import { checkConnectivity } from 'utils/common/utilFunctions';
|
import { checkConnectivity } from 'utils/common/utilFunctions';
|
||||||
import { ErrorHandler } from 'utils/common/errorUtil';
|
import { ErrorHandler } from 'utils/common/errorUtil';
|
||||||
import CryptoWorker from 'utils/crypto/cryptoWorker';
|
import CryptoWorker from 'utils/crypto/cryptoWorker';
|
||||||
|
import * as convert from 'xml-js';
|
||||||
|
import { ENCRYPTION_CHUNK_SIZE } from 'utils/crypto/libsodium';
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
const THUMBNAIL_HEIGHT = 720;
|
const THUMBNAIL_HEIGHT = 720;
|
||||||
|
const MAX_URL_REQUESTS = 50;
|
||||||
const MAX_ATTEMPTS = 3;
|
const MAX_ATTEMPTS = 3;
|
||||||
const MIN_THUMBNAIL_SIZE = 50000;
|
const MIN_THUMBNAIL_SIZE = 50000;
|
||||||
const MAX_CONCURRENT_UPLOADS = 4;
|
const MAX_CONCURRENT_UPLOADS = 4;
|
||||||
|
@ -18,7 +21,17 @@ const TYPE_VIDEO = 'video';
|
||||||
const TYPE_JSON = 'json';
|
const TYPE_JSON = 'json';
|
||||||
const SOUTH_DIRECTION = 'S';
|
const SOUTH_DIRECTION = 'S';
|
||||||
const WEST_DIRECTION = 'W';
|
const WEST_DIRECTION = 'W';
|
||||||
|
const MIN_STREAM_FILE_SIZE = 20 * 1024 * 1024;
|
||||||
|
const CHUNKS_COMBINED_FOR_UPLOAD = 2;
|
||||||
|
|
||||||
|
export interface DataStream {
|
||||||
|
stream: ReadableStream<Uint8Array>;
|
||||||
|
chunkCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDataStream(object: any): object is DataStream {
|
||||||
|
return object.hasOwnProperty('stream');
|
||||||
|
}
|
||||||
interface EncryptionResult {
|
interface EncryptionResult {
|
||||||
file: fileAttribute;
|
file: fileAttribute;
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -34,6 +47,12 @@ interface UploadURL {
|
||||||
objectKey: string;
|
objectKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MultipartUploadURLs {
|
||||||
|
objectKey: string;
|
||||||
|
partURLs: string[];
|
||||||
|
completeURL: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MetadataObject {
|
export interface MetadataObject {
|
||||||
title: string;
|
title: string;
|
||||||
creationTime: number;
|
creationTime: number;
|
||||||
|
@ -43,8 +62,8 @@ export interface MetadataObject {
|
||||||
fileType: FILE_TYPE;
|
fileType: FILE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileinMemory {
|
interface FileInMemory {
|
||||||
filedata: Uint8Array;
|
filedata: Uint8Array | DataStream;
|
||||||
thumbnail: Uint8Array;
|
thumbnail: Uint8Array;
|
||||||
metadata: MetadataObject;
|
metadata: MetadataObject;
|
||||||
}
|
}
|
||||||
|
@ -86,7 +105,7 @@ class UploadService {
|
||||||
private setUploadErrors;
|
private setUploadErrors;
|
||||||
|
|
||||||
public async uploadFiles(
|
public async uploadFiles(
|
||||||
recievedFiles: File[],
|
receivedFiles: File[],
|
||||||
collection: collection,
|
collection: collection,
|
||||||
token: string,
|
token: string,
|
||||||
progressBarProps,
|
progressBarProps,
|
||||||
|
@ -104,7 +123,7 @@ class UploadService {
|
||||||
|
|
||||||
let metadataFiles: File[] = [];
|
let metadataFiles: File[] = [];
|
||||||
let actualFiles: File[] = [];
|
let actualFiles: File[] = [];
|
||||||
recievedFiles.forEach((file) => {
|
receivedFiles.forEach((file) => {
|
||||||
if (
|
if (
|
||||||
file.type.substr(0, 5) === TYPE_IMAGE ||
|
file.type.substr(0, 5) === TYPE_IMAGE ||
|
||||||
file.type.substr(0, 5) === TYPE_VIDEO
|
file.type.substr(0, 5) === TYPE_VIDEO
|
||||||
|
@ -131,6 +150,7 @@ class UploadService {
|
||||||
try {
|
try {
|
||||||
await this.fetchUploadURLs(token);
|
await this.fetchUploadURLs(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('error fetching uploadURLs', e);
|
||||||
ErrorHandler(e);
|
ErrorHandler(e);
|
||||||
}
|
}
|
||||||
const uploadProcesses = [];
|
const uploadProcesses = [];
|
||||||
|
@ -153,11 +173,13 @@ class UploadService {
|
||||||
progressBarProps.setUploadStage(UPLOAD_STAGES.FINISH);
|
progressBarProps.setUploadStage(UPLOAD_STAGES.FINISH);
|
||||||
progressBarProps.setPercentComplete(100);
|
progressBarProps.setPercentComplete(100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('uploading failed with error', e);
|
||||||
this.filesToBeUploaded = [];
|
this.filesToBeUploaded = [];
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploader(
|
private async uploader(
|
||||||
worker: any,
|
worker: any,
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
|
@ -166,7 +188,7 @@ class UploadService {
|
||||||
token: string
|
token: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
let file: FileinMemory = await this.readFile(reader, rawFile);
|
let file: FileInMemory = await this.readFile(reader, rawFile);
|
||||||
let {
|
let {
|
||||||
file: encryptedFile,
|
file: encryptedFile,
|
||||||
fileKey: encryptedKey,
|
fileKey: encryptedKey,
|
||||||
|
@ -175,13 +197,13 @@ class UploadService {
|
||||||
file,
|
file,
|
||||||
collection.key
|
collection.key
|
||||||
);
|
);
|
||||||
file = null;
|
let backupedFile: BackupedFile = await this.uploadToBucket(
|
||||||
let backupedFile: BackupedFile = await this.uploadtoBucket(
|
|
||||||
encryptedFile,
|
encryptedFile,
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
file = null;
|
||||||
encryptedFile = null;
|
encryptedFile = null;
|
||||||
let uploadFile: uploadFile = this.getuploadFile(
|
let uploadFile: uploadFile = this.getUploadFile(
|
||||||
collection,
|
collection,
|
||||||
backupedFile,
|
backupedFile,
|
||||||
encryptedKey
|
encryptedKey
|
||||||
|
@ -193,6 +215,7 @@ class UploadService {
|
||||||
this.filesCompleted++;
|
this.filesCompleted++;
|
||||||
this.changeProgressBarProps();
|
this.changeProgressBarProps();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('file upload failed with error', e);
|
||||||
ErrorHandler(e);
|
ErrorHandler(e);
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`Uploading Failed for File - ${rawFile.name}`
|
`Uploading Failed for File - ${rawFile.name}`
|
||||||
|
@ -220,19 +243,15 @@ class UploadService {
|
||||||
this.setUploadErrors(this.uploadErrors);
|
this.setUploadErrors(this.uploadErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readFile(reader: FileReader, recievedFile: File) {
|
private async readFile(reader: FileReader, receivedFile: File) {
|
||||||
try {
|
try {
|
||||||
const filedata: Uint8Array = await this.getUint8ArrayView(
|
|
||||||
reader,
|
|
||||||
recievedFile
|
|
||||||
);
|
|
||||||
const thumbnail = await this.generateThumbnail(
|
const thumbnail = await this.generateThumbnail(
|
||||||
reader,
|
reader,
|
||||||
recievedFile
|
receivedFile
|
||||||
);
|
);
|
||||||
|
|
||||||
let fileType: FILE_TYPE;
|
let fileType: FILE_TYPE;
|
||||||
switch (recievedFile.type.split('/')[0]) {
|
switch (receivedFile.type.split('/')[0]) {
|
||||||
case TYPE_IMAGE:
|
case TYPE_IMAGE:
|
||||||
fileType = FILE_TYPE.IMAGE;
|
fileType = FILE_TYPE.IMAGE;
|
||||||
break;
|
break;
|
||||||
|
@ -245,20 +264,26 @@ class UploadService {
|
||||||
|
|
||||||
const { location, creationTime } = await this.getExifData(
|
const { location, creationTime } = await this.getExifData(
|
||||||
reader,
|
reader,
|
||||||
recievedFile
|
receivedFile,
|
||||||
|
fileType
|
||||||
);
|
);
|
||||||
const metadata = Object.assign(
|
const metadata = Object.assign(
|
||||||
{
|
{
|
||||||
title: recievedFile.name,
|
title: receivedFile.name,
|
||||||
creationTime:
|
creationTime:
|
||||||
creationTime || recievedFile.lastModified * 1000,
|
creationTime || receivedFile.lastModified * 1000,
|
||||||
modificationTime: recievedFile.lastModified * 1000,
|
modificationTime: receivedFile.lastModified * 1000,
|
||||||
latitude: location?.latitude,
|
latitude: location?.latitude,
|
||||||
longitude: location?.latitude,
|
longitude: location?.latitude,
|
||||||
fileType,
|
fileType,
|
||||||
},
|
},
|
||||||
this.metadataMap.get(recievedFile.name)
|
this.metadataMap.get(receivedFile.name)
|
||||||
);
|
);
|
||||||
|
const filedata =
|
||||||
|
receivedFile.size > MIN_STREAM_FILE_SIZE
|
||||||
|
? this.getFileStream(reader, receivedFile)
|
||||||
|
: await this.getUint8ArrayView(reader, receivedFile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filedata,
|
filedata,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
@ -269,16 +294,19 @@ class UploadService {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async encryptFile(
|
private async encryptFile(
|
||||||
worker,
|
worker: any,
|
||||||
file: FileinMemory,
|
file: FileInMemory,
|
||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<EncryptedFile> {
|
): Promise<EncryptedFile> {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
key: fileKey,
|
key: fileKey,
|
||||||
file: encryptedFiledata,
|
file: encryptedFiledata,
|
||||||
}: EncryptionResult = await worker.encryptFile(file.filedata);
|
}: EncryptionResult = isDataStream(file.filedata)
|
||||||
|
? await this.encryptFileStream(worker, file.filedata)
|
||||||
|
: await worker.encryptFile(file.filedata);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
file: encryptedThumbnail,
|
file: encryptedThumbnail,
|
||||||
|
@ -313,21 +341,65 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadtoBucket(
|
private async encryptFileStream(worker, fileData: DataStream) {
|
||||||
|
const { stream, chunkCount } = fileData;
|
||||||
|
const fileStreamReader = stream.getReader();
|
||||||
|
const {
|
||||||
|
key,
|
||||||
|
decryptionHeader,
|
||||||
|
pushState,
|
||||||
|
} = await worker.initChunkEncryption();
|
||||||
|
let ref = { pullCount: 1 };
|
||||||
|
const encryptedFileStream = new ReadableStream({
|
||||||
|
async pull(controller) {
|
||||||
|
let { value } = await fileStreamReader.read();
|
||||||
|
const encryptedFileChunk = await worker.encryptFileChunk(
|
||||||
|
value,
|
||||||
|
pushState,
|
||||||
|
ref.pullCount === chunkCount
|
||||||
|
);
|
||||||
|
controller.enqueue(encryptedFileChunk);
|
||||||
|
if (ref.pullCount == chunkCount) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
ref.pullCount++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
file: {
|
||||||
|
decryptionHeader,
|
||||||
|
encryptedData: { stream: encryptedFileStream, chunkCount },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadToBucket(
|
||||||
file: ProcessedFile,
|
file: ProcessedFile,
|
||||||
token
|
token: string
|
||||||
): Promise<BackupedFile> {
|
): Promise<BackupedFile> {
|
||||||
try {
|
try {
|
||||||
const fileUploadURL = await this.getUploadURL(token);
|
if (isDataStream(file.file.encryptedData)) {
|
||||||
file.file.objectKey = await this.putFile(
|
const { chunkCount, stream } = file.file.encryptedData;
|
||||||
fileUploadURL,
|
const filePartUploadURLs = await this.fetchMultipartUploadURLs(
|
||||||
file.file.encryptedData
|
token,
|
||||||
);
|
Math.ceil(chunkCount / CHUNKS_COMBINED_FOR_UPLOAD)
|
||||||
|
);
|
||||||
|
file.file.objectKey = await this.putFileInParts(
|
||||||
|
filePartUploadURLs,
|
||||||
|
stream
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const fileUploadURL = await this.getUploadURL(token);
|
||||||
|
file.file.objectKey = await this.putFile(
|
||||||
|
fileUploadURL,
|
||||||
|
file.file.encryptedData
|
||||||
|
);
|
||||||
|
}
|
||||||
const thumbnailUploadURL = await this.getUploadURL(token);
|
const thumbnailUploadURL = await this.getUploadURL(token);
|
||||||
file.thumbnail.objectKey = await this.putFile(
|
file.thumbnail.objectKey = await this.putFile(
|
||||||
thumbnailUploadURL,
|
thumbnailUploadURL,
|
||||||
file.thumbnail.encryptedData
|
file.thumbnail.encryptedData as Uint8Array
|
||||||
);
|
);
|
||||||
delete file.file.encryptedData;
|
delete file.file.encryptedData;
|
||||||
delete file.thumbnail.encryptedData;
|
delete file.thumbnail.encryptedData;
|
||||||
|
@ -339,7 +411,7 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getuploadFile(
|
private getUploadFile(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
backupedFile: BackupedFile,
|
backupedFile: BackupedFile,
|
||||||
fileKey: B64EncryptionResult
|
fileKey: B64EncryptionResult
|
||||||
|
@ -369,19 +441,19 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async seedMetadataMap(recievedFile: File) {
|
private async seedMetadataMap(receivedFile: File) {
|
||||||
try {
|
try {
|
||||||
const metadataJSON: object = await new Promise(
|
const metadataJSON: object = await new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
var result =
|
let result =
|
||||||
typeof reader.result !== 'string'
|
typeof reader.result !== 'string'
|
||||||
? new TextDecoder().decode(reader.result)
|
? new TextDecoder().decode(reader.result)
|
||||||
: reader.result;
|
: reader.result;
|
||||||
resolve(JSON.parse(result));
|
resolve(JSON.parse(result));
|
||||||
};
|
};
|
||||||
reader.readAsText(recievedFile);
|
reader.readAsText(receivedFile);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -391,7 +463,7 @@ class UploadService {
|
||||||
metaDataObject['modificationTime'] =
|
metaDataObject['modificationTime'] =
|
||||||
metadataJSON['modificationTime']['timestamp'] * 1000000;
|
metadataJSON['modificationTime']['timestamp'] * 1000000;
|
||||||
|
|
||||||
var locationData = null;
|
let locationData = null;
|
||||||
if (
|
if (
|
||||||
metadataJSON['geoData']['latitude'] != 0.0 ||
|
metadataJSON['geoData']['latitude'] != 0.0 ||
|
||||||
metadataJSON['geoData']['longitude'] != 0.0
|
metadataJSON['geoData']['longitude'] != 0.0
|
||||||
|
@ -404,7 +476,7 @@ class UploadService {
|
||||||
locationData = metadataJSON['geoDataExif'];
|
locationData = metadataJSON['geoDataExif'];
|
||||||
}
|
}
|
||||||
if (locationData != null) {
|
if (locationData != null) {
|
||||||
metaDataObject['latitude'] = locationData['latitide'];
|
metaDataObject['latitude'] = locationData['latitude'];
|
||||||
metaDataObject['longitude'] = locationData['longitude'];
|
metaDataObject['longitude'] = locationData['longitude'];
|
||||||
}
|
}
|
||||||
this.metadataMap.set(metadataJSON['title'], metaDataObject);
|
this.metadataMap.set(metadataJSON['title'], metaDataObject);
|
||||||
|
@ -507,6 +579,36 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getFileStream(reader: FileReader, file: File) {
|
||||||
|
let self = this;
|
||||||
|
let fileChunkReader = (async function* fileChunkReaderMaker(
|
||||||
|
fileSize,
|
||||||
|
self
|
||||||
|
) {
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < fileSize) {
|
||||||
|
let blob = file.slice(offset, ENCRYPTION_CHUNK_SIZE + offset);
|
||||||
|
let fileChunk = await self.getUint8ArrayView(reader, blob);
|
||||||
|
yield fileChunk;
|
||||||
|
offset += ENCRYPTION_CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})(file.size, self);
|
||||||
|
return {
|
||||||
|
stream: new ReadableStream<Uint8Array>({
|
||||||
|
async pull(controller: ReadableStreamDefaultController) {
|
||||||
|
let chunk = await fileChunkReader.next();
|
||||||
|
if (chunk.done) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.enqueue(chunk.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
chunkCount: Math.ceil(file.size / ENCRYPTION_CHUNK_SIZE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async getUint8ArrayView(
|
private async getUint8ArrayView(
|
||||||
reader: FileReader,
|
reader: FileReader,
|
||||||
file: Blob
|
file: Blob
|
||||||
|
@ -526,7 +628,7 @@ class UploadService {
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('error readinf file to bytearray ', e);
|
console.error('error reading file to byte-array ', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -545,7 +647,7 @@ class UploadService {
|
||||||
`${ENDPOINT}/files/upload-urls`,
|
`${ENDPOINT}/files/upload-urls`,
|
||||||
{
|
{
|
||||||
count: Math.min(
|
count: Math.min(
|
||||||
50,
|
MAX_URL_REQUESTS,
|
||||||
(this.totalFileCount - this.filesCompleted) * 2
|
(this.totalFileCount - this.filesCompleted) * 2
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -563,15 +665,32 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fetchMultipartUploadURLs(
|
||||||
|
token: string,
|
||||||
|
count: number
|
||||||
|
): Promise<MultipartUploadURLs> {
|
||||||
|
try {
|
||||||
|
const response = await HTTPService.get(
|
||||||
|
`${ENDPOINT}/files/multipart-upload-urls`,
|
||||||
|
{
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
{ 'X-Auth-Token': token }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data['urls'];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetch multipart-upload-url failed ', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async putFile(
|
private async putFile(
|
||||||
fileUploadURL: UploadURL,
|
fileUploadURL: UploadURL,
|
||||||
file: Uint8Array | string
|
file: Uint8Array
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const fileSize = file.length;
|
await HTTPService.put(fileUploadURL.url, file);
|
||||||
await HTTPService.put(fileUploadURL.url, file, null, {
|
|
||||||
contentLengthHeader: fileSize,
|
|
||||||
});
|
|
||||||
return fileUploadURL.objectKey;
|
return fileUploadURL.objectKey;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('putFile to dataStore failed ', e);
|
console.error('putFile to dataStore failed ', e);
|
||||||
|
@ -579,13 +698,67 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getExifData(reader: FileReader, recievedFile: File) {
|
private async putFileInParts(
|
||||||
|
multipartUploadURLs: MultipartUploadURLs,
|
||||||
|
file: ReadableStream<Uint8Array>
|
||||||
|
) {
|
||||||
|
let streamEncryptedFileReader = file.getReader();
|
||||||
|
const resParts = [];
|
||||||
|
for (const [
|
||||||
|
index,
|
||||||
|
fileUploadURL,
|
||||||
|
] of multipartUploadURLs.partURLs.entries()) {
|
||||||
|
let {
|
||||||
|
done: done1,
|
||||||
|
value: chunk1,
|
||||||
|
} = await streamEncryptedFileReader.read();
|
||||||
|
if (done1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let {
|
||||||
|
done: done2,
|
||||||
|
value: chunk2,
|
||||||
|
} = await streamEncryptedFileReader.read();
|
||||||
|
let uploadChunk: Uint8Array;
|
||||||
|
if (!done2) {
|
||||||
|
uploadChunk = new Uint8Array(chunk1.length + chunk2.length);
|
||||||
|
uploadChunk.set(chunk1);
|
||||||
|
uploadChunk.set(chunk2, chunk1.length);
|
||||||
|
} else {
|
||||||
|
uploadChunk = chunk1;
|
||||||
|
}
|
||||||
|
const response = await HTTPService.put(fileUploadURL, uploadChunk);
|
||||||
|
resParts.push({
|
||||||
|
PartNumber: index + 1,
|
||||||
|
ETag: response.headers.etag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var options = { compact: true, ignoreComment: true, spaces: 4 };
|
||||||
|
const body = convert.js2xml(
|
||||||
|
{ CompleteMultipartUpload: { Part: resParts } },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
await HTTPService.post(multipartUploadURLs.completeURL, body, null, {
|
||||||
|
'content-type': 'text/xml',
|
||||||
|
});
|
||||||
|
return multipartUploadURLs.objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getExifData(
|
||||||
|
reader: FileReader,
|
||||||
|
receivedFile: File,
|
||||||
|
fileType: FILE_TYPE
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
|
if (fileType === FILE_TYPE.VIDEO) {
|
||||||
|
// Todo extract exif data from videos
|
||||||
|
return { location: null, creationTime: null };
|
||||||
|
}
|
||||||
const exifData: any = await new Promise((resolve, reject) => {
|
const exifData: any = await new Promise((resolve, reject) => {
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
resolve(EXIF.readFromBinaryFile(reader.result));
|
resolve(EXIF.readFromBinaryFile(reader.result));
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(recievedFile);
|
reader.readAsArrayBuffer(receivedFile);
|
||||||
});
|
});
|
||||||
if (!exifData) {
|
if (!exifData) {
|
||||||
return { location: null, creationTime: null };
|
return { location: null, creationTime: null };
|
||||||
|
@ -604,8 +777,8 @@ class UploadService {
|
||||||
if (!dateString) {
|
if (!dateString) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var parts = dateString.split(' ')[0].split(':');
|
let parts = dateString.split(' ')[0].split(':');
|
||||||
var date = new Date(
|
let date = new Date(
|
||||||
Number(parts[0]),
|
Number(parts[0]),
|
||||||
Number(parts[1]) - 1,
|
Number(parts[1]) - 1,
|
||||||
Number(parts[2])
|
Number(parts[2])
|
||||||
|
@ -629,17 +802,17 @@ class UploadService {
|
||||||
lonMinute = exifData.GPSLongitude[1];
|
lonMinute = exifData.GPSLongitude[1];
|
||||||
lonSecond = exifData.GPSLongitude[2];
|
lonSecond = exifData.GPSLongitude[2];
|
||||||
|
|
||||||
var latDirection = exifData.GPSLatitudeRef;
|
let latDirection = exifData.GPSLatitudeRef;
|
||||||
var lonDirection = exifData.GPSLongitudeRef;
|
let lonDirection = exifData.GPSLongitudeRef;
|
||||||
|
|
||||||
var latFinal = this.convertDMSToDD(
|
let latFinal = this.convertDMSToDD(
|
||||||
latDegree,
|
latDegree,
|
||||||
latMinute,
|
latMinute,
|
||||||
latSecond,
|
latSecond,
|
||||||
latDirection
|
latDirection
|
||||||
);
|
);
|
||||||
|
|
||||||
var lonFinal = this.convertDMSToDD(
|
let lonFinal = this.convertDMSToDD(
|
||||||
lonDegree,
|
lonDegree,
|
||||||
lonMinute,
|
lonMinute,
|
||||||
lonSecond,
|
lonSecond,
|
||||||
|
@ -649,7 +822,7 @@ class UploadService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertDMSToDD(degrees, minutes, seconds, direction) {
|
private convertDMSToDD(degrees, minutes, seconds, direction) {
|
||||||
var dd = degrees + minutes / 60 + seconds / 3600;
|
let dd = degrees + minutes / 60 + seconds / 3600;
|
||||||
|
|
||||||
if (direction == SOUTH_DIRECTION || direction == WEST_DIRECTION) {
|
if (direction == SOUTH_DIRECTION || direction == WEST_DIRECTION) {
|
||||||
dd = dd * -1;
|
dd = dd * -1;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sodium, { StateAddress } from 'libsodium-wrappers';
|
import sodium, { StateAddress } from 'libsodium-wrappers';
|
||||||
|
|
||||||
const encryptionChunkSize = 4 * 1024 * 1024;
|
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
|
||||||
|
|
||||||
export async function decryptChaChaOneShot(
|
export async function decryptChaChaOneShot(
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
|
@ -31,7 +31,7 @@ export async function decryptChaCha(
|
||||||
await fromB64(key)
|
await fromB64(key)
|
||||||
);
|
);
|
||||||
const decryptionChunkSize =
|
const decryptionChunkSize =
|
||||||
encryptionChunkSize +
|
ENCRYPTION_CHUNK_SIZE +
|
||||||
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||||
var bytesRead = 0;
|
var bytesRead = 0;
|
||||||
var decryptedData = [];
|
var decryptedData = [];
|
||||||
|
@ -57,16 +57,23 @@ export async function decryptChaCha(
|
||||||
|
|
||||||
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
|
export async function initChunkDecryption(header: Uint8Array, key: Uint8Array) {
|
||||||
await sodium.ready;
|
await sodium.ready;
|
||||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
|
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||||
|
header,
|
||||||
|
key
|
||||||
|
);
|
||||||
const decryptionChunkSize =
|
const decryptionChunkSize =
|
||||||
encryptionChunkSize + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
ENCRYPTION_CHUNK_SIZE +
|
||||||
|
sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||||
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
const tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||||
return { pullState, decryptionChunkSize, tag };
|
return { pullState, decryptionChunkSize, tag };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptChunk(data: Uint8Array, pullState: StateAddress) {
|
export async function decryptChunk(data: Uint8Array, pullState: StateAddress) {
|
||||||
await sodium.ready;
|
await sodium.ready;
|
||||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(pullState, data);
|
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||||
|
pullState,
|
||||||
|
data
|
||||||
|
);
|
||||||
const newTag = pullResult.tag;
|
const newTag = pullResult.tag;
|
||||||
return { decryptedData: pullResult.message, newTag };
|
return { decryptedData: pullResult.message, newTag };
|
||||||
}
|
}
|
||||||
|
@ -114,7 +121,7 @@ export async function encryptChaCha(data: Uint8Array, key?: string) {
|
||||||
let encryptedData = [];
|
let encryptedData = [];
|
||||||
|
|
||||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||||
let chunkSize = encryptionChunkSize;
|
let chunkSize = ENCRYPTION_CHUNK_SIZE;
|
||||||
if (bytesRead + chunkSize >= data.length) {
|
if (bytesRead + chunkSize >= data.length) {
|
||||||
chunkSize = data.length - bytesRead;
|
chunkSize = data.length - bytesRead;
|
||||||
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||||
|
@ -141,6 +148,38 @@ export async function encryptChaCha(data: Uint8Array, key?: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initChunkEncryption() {
|
||||||
|
await sodium.ready;
|
||||||
|
let key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||||
|
let initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(
|
||||||
|
key
|
||||||
|
);
|
||||||
|
let [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||||
|
return {
|
||||||
|
key: await toB64(key),
|
||||||
|
decryptionHeader: await toB64(header),
|
||||||
|
pushState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function encryptFileChunk(
|
||||||
|
data: Uint8Array,
|
||||||
|
pushState: sodium.StateAddress,
|
||||||
|
finalChunk?: boolean
|
||||||
|
) {
|
||||||
|
await sodium.ready;
|
||||||
|
|
||||||
|
let tag = finalChunk
|
||||||
|
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
|
||||||
|
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||||
|
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||||
|
pushState,
|
||||||
|
data,
|
||||||
|
null,
|
||||||
|
tag
|
||||||
|
);
|
||||||
|
|
||||||
|
return pushResult;
|
||||||
|
}
|
||||||
export async function encryptToB64(data: string, key?: string) {
|
export async function encryptToB64(data: string, key?: string) {
|
||||||
await sodium.ready;
|
await sodium.ready;
|
||||||
const encrypted = await encrypt(
|
const encrypted = await encrypt(
|
||||||
|
|
|
@ -44,6 +44,13 @@ export class Crypto {
|
||||||
async encryptFile(fileData, key) {
|
async encryptFile(fileData, key) {
|
||||||
return libsodium.encryptChaCha(fileData, key);
|
return libsodium.encryptChaCha(fileData, key);
|
||||||
}
|
}
|
||||||
|
async encryptFileChunk(data, pushState, finalChunk) {
|
||||||
|
return libsodium.encryptFileChunk(data, pushState, finalChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initChunkEncryption() {
|
||||||
|
return libsodium.initChunkEncryption();
|
||||||
|
}
|
||||||
|
|
||||||
async initDecryption(header, key) {
|
async initDecryption(header, key) {
|
||||||
return libsodium.initChunkDecryption(header, key);
|
return libsodium.initChunkDecryption(header, key);
|
||||||
|
|
|
@ -7792,6 +7792,13 @@ xhr@^2.0.1:
|
||||||
parse-headers "^2.0.0"
|
parse-headers "^2.0.0"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
|
xml-js@^1.6.11:
|
||||||
|
version "1.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
|
||||||
|
integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
|
||||||
|
dependencies:
|
||||||
|
sax "^1.2.4"
|
||||||
|
|
||||||
xml-parse-from-string@^1.0.0:
|
xml-parse-from-string@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
|
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
|
||||||
|
|
Loading…
Reference in a new issue