From 88e715eb60792139433b0d80688f94c22e5e2abe Mon Sep 17 00:00:00 2001 From: Richard Park Date: Thu, 7 Nov 2019 11:37:33 +0900 Subject: [PATCH] file download is implemented --- main/src/index.ts | 33 ++- main/src/lib/default-folder.ts | 37 +++ main/src/lib/file-util.ts | 222 ++++++++++++++++++ .../dialogs/file-viewer.dialog.component.ts | 32 ++- .../src/lib/utils/file.util.ts | 33 ++- .../lib/services/browser-native.service.ts | 16 +- .../src/lib/translate/browser-loader.ts | 5 +- .../lib/services/electron-native.service.ts | 16 +- .../src/lib/services/native.service.ts | 4 +- .../file-viewer/image-viewer.component.ts | 2 +- 10 files changed, 374 insertions(+), 26 deletions(-) create mode 100644 main/src/lib/default-folder.ts create mode 100644 main/src/lib/file-util.ts diff --git a/main/src/index.ts b/main/src/index.ts index 4cbd0ebe..6476845f 100644 --- a/main/src/index.ts +++ b/main/src/index.ts @@ -2,12 +2,15 @@ import { app, ipcMain, IpcMainEvent, remote } from 'electron'; import * as path from 'path'; import * as url from 'url'; import * as fse from 'fs-extra'; +import * as fs from 'fs'; import { AppWindow } from './app/AppWindow'; import { now } from './util/now'; import { showUncaughtException } from './crash/show-uncaught-exception'; import { Channel } from '@ucap-webmessenger/native-electron'; import { root } from './util/root'; +import { DefaultFolder } from './lib/default-folder'; +import { FileUtil } from './lib/file-util'; let appWindow: AppWindow | null = null; @@ -179,19 +182,37 @@ ipcMain.on(Channel.checkForUpdates, (event: IpcMainEvent, ...args: any[]) => { ipcMain.on(Channel.readFile, (event: IpcMainEvent, ...args: any[]) => { try { - const rBuf = fse.readFileSync(root(args[0])); - event.returnValue = rBuf.buffer; + fse.readFile(root(args[0]), (err, data) => { + if (!!err) { + event.returnValue = null; + } else { + event.returnValue = new Blob([data]); + } + }); } catch (error) { event.returnValue = null; } }); -ipcMain.on(Channel.saveFile, (event: IpcMainEvent, ...args: any[]) => { +ipcMain.on(Channel.saveFile, async (event: IpcMainEvent, ...args: any[]) => { try { - fse.writeFileSync(root(args[0]), args[1]); - event.returnValue = true; + const buffer: Buffer = args[0]; + const fileName: string = args[1]; + let savePath: string = path.join( + !!args[2] ? args[2] : DefaultFolder.downloads(), + fileName + ); + savePath = await FileUtil.uniqueFileName(savePath); + + fse.writeFile(savePath, buffer, err => { + if (!err) { + event.returnValue = savePath; + } else { + event.returnValue = undefined; + } + }); } catch (error) { - event.returnValue = false; + event.returnValue = undefined; } }); diff --git a/main/src/lib/default-folder.ts b/main/src/lib/default-folder.ts new file mode 100644 index 00000000..6a8e659d --- /dev/null +++ b/main/src/lib/default-folder.ts @@ -0,0 +1,37 @@ +import * as os from 'os'; +import { execSync } from 'child_process'; +import * as fse from 'fs-extra'; + +export class DefaultFolder { + static downloads(): string { + switch (os.platform()) { + case 'win32': + return `${process.env.USERPROFILE}/Downloads`; + case 'darwin': + return `${process.env.HOME}/Downloads`; + case 'linux': { + let dir: Buffer; + try { + dir = execSync('xdg-user-dir DOWNLOAD', { stdio: [0, 3, 3] }); + } catch (_) {} + + if (dir) { + return dir.toString('utf-8'); + } + + let stat: fse.Stats; + const homeDownloads = `${process.env.HOME}/Downloads`; + try { + stat = fse.statSync(homeDownloads); + } catch (_) {} + if (stat) { + return homeDownloads; + } + return '/tmp/'; + } + + default: + break; + } + } +} diff --git a/main/src/lib/file-util.ts b/main/src/lib/file-util.ts new file mode 100644 index 00000000..64e46a28 --- /dev/null +++ b/main/src/lib/file-util.ts @@ -0,0 +1,222 @@ +import * as path from 'path'; +import * as fse from 'fs-extra'; + +/** + * + * separator + * If the specified filename exists, the separator will be added before the incremental value such as: file{separator}2.jpg + * The default value is '-'. + * + * mode + * The mode allows you to specify which characters to use to generate the incremental value (the string after the separator) + * The default value is 'numeric'. + * 'numeric' Using the following characters: 1234567890 + * 'alpha' Using the following characters: abcdefghijklmnopqrstuvwxyz + * 'ALPHA' Using the following characters: ABCDEFGHIJKLMNOPQRSTUVWXYZ + * 'alphanumeric' Using the following characters: 0123456789abcdefghijklmnopqrstuvwxyz + * 'ALPHANUMERIC' Using the following characters: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ + * 'charset' You must specify the characters you wish to use in the charset option + * + * paddingCharacter && paddingSize + * If you wish to left-pad the incremental values with a character, use this option. Here's an example : + * var uniquefilename = require('uniquefilename'); + * options = {mode: 'alpha', paddingCharacter: '0', paddingSize: 3}; + * uniquefilename.get('/path/to/dir/file.jpg', options, function(filename) { + * // filename might be "/path/to/dir/file.jpg", + * // "/path/to/dir/file-002.jpg", "/path/to/dir/file-045.jpg", etc... + * // depending on the files that exist on your filesystem + * }); + * + * alwaysAppend + * If alwaysAppend is true filenames will include the separator and attachment from the first request. + * So instead of file.jpg, file-2.jpg you'd get file-1.jpg, file-2.jpg. + */ +export interface UniqueFileNameOption { + separator?: string; + mode?: + | 'numeric' + | 'alpha' + | 'ALPHA' + | 'alphanumeric' + | 'ALPHANUMERIC' + | 'charset'; + paddingCharacter?: string; + paddingSize?: number; + alwaysAppend?: boolean; + charset?: string; +} + +const charsets = { + alpha: 'abcdefghijklmnopqrstuvwxyz', + alphanumeric: '0123456789abcdefghijklmnopqrstuvwxyz', + ALPHA: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + ALPHANUMERIC: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' +}; + +interface UniqueFile { + dir?: string; + ext?: string; + base?: string; + increment?: number; +} + +export class FileUtil { + static blobToBuffer(blob: Blob): Promise { + if (typeof Blob === 'undefined' || !(blob instanceof Blob)) { + throw new Error('first argument must be a Blob'); + } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + resolve(Buffer.from(reader.result as ArrayBuffer)); + }; + reader.onerror = () => { + reader.abort(); + reject(reader.error); + }; + reader.readAsArrayBuffer(blob); + }); + } + + static uniqueFileName( + filePath: string, + options?: UniqueFileNameOption + ): Promise { + return new Promise((resolve, reject) => { + const dir = path.dirname(filePath); + const ext = path.extname(filePath); + const base = path.basename(filePath, ext); + + const uniqueFile: UniqueFile = { + dir, + ext, + base + }; + + options = options || {}; + options.separator = options.separator || '-'; + options.mode = options.mode || 'numeric'; + + if ('numeric' !== options.mode) { + if (charsets[options.mode]) { + options.charset = charsets[options.mode]; + options.mode = 'charset'; + } else if ( + 'charset' !== options.mode || + ('charset' === options.mode && !options.charset) + ) { + options.mode = 'numeric'; + } + } + + if (options.paddingSize && !options.paddingCharacter) { + options.paddingCharacter = '0'; + } + + FileUtil.uniqueFileNameProcess( + uniqueFile, + options, + (fileName: string) => { + resolve(fileName); + } + ); + }); + } + + private static uniqueFileNameProcess( + uniqueFile: UniqueFile, + options: UniqueFileNameOption, + callback: (fileName: string) => void + ) { + let fileName: string; + let append = ''; + + if (options.alwaysAppend && !uniqueFile.increment) { + uniqueFile.increment = 1; + } + + if (uniqueFile.increment) { + if ('numeric' === options.mode) { + append = '' + uniqueFile.increment; + } else { + append = FileUtil.numberToString(uniqueFile.increment, options.charset); + } + + if (options.paddingSize) { + while (append.length < options.paddingSize) { + append = options.paddingCharacter + append; + } + } + + append = options.separator + append; + } + + fileName = path.join( + uniqueFile.dir, + uniqueFile.base + append + uniqueFile.ext + ); + if (fse.existsSync(fileName)) { + if (uniqueFile.increment) { + uniqueFile.increment += 1; + } else { + uniqueFile.increment = 'numeric' === options.mode ? 2 : 1; + } + return FileUtil.uniqueFileNameProcess(uniqueFile, options, callback); + } else { + return callback(fileName); + } + } + + private static numberToString(nbr: number, charset: string) { + const charsetLen = charset.length; + let strLen = 0; + let strThisLen = 0; + let tmp: number; + + for (let maxpower = 20; maxpower >= 0; maxpower--) { + const maxvalue = FileUtil.sumOfPowerFromOne(charsetLen, maxpower); + + if (maxvalue < nbr) { + strLen = maxpower + 1; + strThisLen = maxvalue + Math.pow(charsetLen, maxpower + 1) - maxvalue; + + break; + } + } + + if (0 === strLen) { + return null; + } + + let str = ''; + while (--strLen >= 0) { + if (strLen === 0) { + str += charset.charAt(nbr - 1); + break; + } + + strThisLen = Math.pow(charsetLen, strLen); + const initial = FileUtil.sumOfPowerFromOne(charsetLen, strLen - 1); + + for (tmp = charsetLen; tmp >= 1; tmp--) { + if (initial + tmp * strThisLen < nbr) { + break; + } + } + + nbr -= tmp * strThisLen; + str += charset.charAt(tmp - 1); + } + + return str; + } + + private static sumOfPowerFromOne(base: number, maxpower: number) { + let value = 0; + for (let tmp = maxpower; tmp >= 1; tmp--) { + value += Math.pow(base, tmp); + } + return value; + } +} diff --git a/projects/ucap-webmessenger-app/src/app/layouts/common/dialogs/file-viewer.dialog.component.ts b/projects/ucap-webmessenger-app/src/app/layouts/common/dialogs/file-viewer.dialog.component.ts index cdbe1ac4..b0cc6383 100644 --- a/projects/ucap-webmessenger-app/src/app/layouts/common/dialogs/file-viewer.dialog.component.ts +++ b/projects/ucap-webmessenger-app/src/app/layouts/common/dialogs/file-viewer.dialog.component.ts @@ -9,8 +9,10 @@ import { import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { NGXLogger } from 'ngx-logger'; import { FileEventJson } from '@ucap-webmessenger/protocol-event'; -import { DeviceType } from '@ucap-webmessenger/core'; -import * as FileSaver from 'file-saver'; +import { DeviceType, FileUtil } from '@ucap-webmessenger/core'; +import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native'; +import { take } from 'rxjs/operators'; +import { SnackBarService } from '@ucap-webmessenger/ui'; export interface FileViewerDialogData { fileInfo: FileEventJson; @@ -40,6 +42,8 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy { FileViewerDialogResult >, @Inject(MAT_DIALOG_DATA) public data: FileViewerDialogData, + @Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService, + private snackBarService: SnackBarService, private logger: NGXLogger ) { this.fileInfo = data.fileInfo; @@ -54,7 +58,29 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy { ngOnDestroy(): void {} onDownload(blob: Blob): void { - FileSaver.saveAs(blob, this.fileInfo.fileName); + FileUtil.fromBlobToBuffer(blob) + .then(buffer => { + this.nativeService + .saveFile(buffer, this.fileInfo.fileName) + .pipe(take(1)) + .subscribe(result => { + if (!!result) { + this.snackBarService.open( + `파일이 경로[${result}]에 저장되었습니다.`, + '', + { + duration: 3000, + verticalPosition: 'bottom' + } + ); + } else { + this.snackBarService.open('파일 저장에 실패하였습니다.'); + } + }); + }) + .catch(reason => { + this.logger.error('download', reason); + }); } onClosedViewer(): void { diff --git a/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts b/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts index 2d215ea3..7f4411c8 100644 --- a/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts +++ b/projects/ucap-webmessenger-core/src/lib/utils/file.util.ts @@ -14,11 +14,9 @@ export class FileUtil { return extensions; } - static toDataUrl(blob: Blob): Promise { + static fromBlobToDataUrl(blob: Blob): Promise { return new Promise((resolve, reject) => { const fileReader = new FileReader(); - fileReader.readAsDataURL(blob); - fileReader.onload = () => { return resolve(fileReader.result); }; @@ -26,6 +24,35 @@ export class FileUtil { fileReader.abort(); return reject(fileReader.error); }; + fileReader.readAsDataURL(blob); + }); + } + + static fromBlobToBuffer(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = () => { + return resolve(Buffer.from(fileReader.result as ArrayBuffer)); + }; + fileReader.onerror = (event: ProgressEvent) => { + fileReader.abort(); + return reject(fileReader.error); + }; + fileReader.readAsArrayBuffer(blob); + }); + } + + static fromBlobToString(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = () => { + return resolve(fileReader.result.toString()); + }; + fileReader.onerror = (event: ProgressEvent) => { + fileReader.abort(); + return reject(fileReader.error); + }; + fileReader.readAsArrayBuffer(blob); }); } } diff --git a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts index 9c73c342..1c3d3214 100644 --- a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts +++ b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts @@ -25,12 +25,20 @@ export class BrowserNativeService implements NativeService { showImageViewer(): void {} - readFile(path: string): Observable { - return this.httpClient.get(path, { responseType: 'arraybuffer' }); + readFile(path: string): Observable { + return this.httpClient.get(path, { responseType: 'arraybuffer' }).pipe( + map(arrayBuffer => { + return Buffer.from(arrayBuffer); + }) + ); } - saveFile(path: string, buf: ArrayBuffer): Observable { - return this.httpClient.post(path, buf, {}); + saveFile( + buffer: Buffer, + fileName: string, + path?: string + ): Observable { + return this.httpClient.post(path, buffer, {}); } windowStateChanged(): Observable { diff --git a/projects/ucap-webmessenger-native-browser/src/lib/translate/browser-loader.ts b/projects/ucap-webmessenger-native-browser/src/lib/translate/browser-loader.ts index d927e114..75e2bb37 100644 --- a/projects/ucap-webmessenger-native-browser/src/lib/translate/browser-loader.ts +++ b/projects/ucap-webmessenger-native-browser/src/lib/translate/browser-loader.ts @@ -2,6 +2,7 @@ import { TranslateLoader } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { NativeService } from '@ucap-webmessenger/native'; import { take, map } from 'rxjs/operators'; +import { FileUtil } from '@ucap-webmessenger/core'; export class TranslateBrowserLoader implements TranslateLoader { constructor( @@ -18,8 +19,8 @@ export class TranslateBrowserLoader implements TranslateLoader { .readFile(`${this.prefix}${lang}.${this.suffix}`) .pipe( take(1), - map(buf => { - return JSON.parse(Buffer.from(buf).toString('utf8')); + map(buffer => { + return JSON.parse(buffer.toString('utf-8')); }) ); } diff --git a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts index 28b1e5a8..39a41285 100644 --- a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts +++ b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts @@ -42,8 +42,8 @@ export class ElectronNativeService implements NativeService { ipcRenderer.send(Channel.showImageViewer); } - readFile(path: string): Observable { - return new Observable(subscriber => { + readFile(path: string): Observable { + return new Observable(subscriber => { try { subscriber.next(ipcRenderer.sendSync(Channel.readFile, path)); } catch (error) { @@ -54,10 +54,16 @@ export class ElectronNativeService implements NativeService { }); } - saveFile(path: string, buf: ArrayBuffer): Observable { - return new Observable(subscriber => { + saveFile( + buffer: Buffer, + fileName: string, + path?: string + ): Observable { + return new Observable(subscriber => { try { - subscriber.next(ipcRenderer.sendSync(Channel.saveFile, path, buf)); + subscriber.next( + ipcRenderer.sendSync(Channel.saveFile, buffer, fileName, path) + ); } catch (error) { subscriber.error(error); } finally { diff --git a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts index 1944c2f8..4518cef0 100644 --- a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts +++ b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts @@ -9,8 +9,8 @@ export interface NativeService { showImageViewer(): void; - saveFile(path: string, buf: ArrayBuffer): Observable; - readFile(path: string): Observable; + saveFile(buffer: Buffer, fileName: string, path?: string): Observable; + readFile(path: string): Observable; windowStateChanged(): Observable; windowClose(): void; diff --git a/projects/ucap-webmessenger-ui/src/lib/components/file-viewer/image-viewer.component.ts b/projects/ucap-webmessenger-ui/src/lib/components/file-viewer/image-viewer.component.ts index c766fed1..e680e572 100644 --- a/projects/ucap-webmessenger-ui/src/lib/components/file-viewer/image-viewer.component.ts +++ b/projects/ucap-webmessenger-ui/src/lib/components/file-viewer/image-viewer.component.ts @@ -69,7 +69,7 @@ export class ImageViewerComponent implements OnInit { blob.size, MimeUtil.getMimeFromExtension(this.fileInfo.fileExt) ); - this.imageSrc = await FileUtil.toDataUrl(this.blob); + this.imageSrc = await FileUtil.fromBlobToDataUrl(this.blob); }) ) .subscribe();