file download is implemented

This commit is contained in:
병준 박 2019-11-07 11:37:33 +09:00
parent 958295a8c1
commit 88e715eb60
10 changed files with 374 additions and 26 deletions

View File

@ -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;
}
});

View File

@ -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;
}
}
}

222
main/src/lib/file-util.ts Normal file
View File

@ -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<Buffer> {
if (typeof Blob === 'undefined' || !(blob instanceof Blob)) {
throw new Error('first argument must be a Blob');
}
return new Promise<Buffer>((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<string> {
return new Promise<string>((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;
}
}

View File

@ -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 {

View File

@ -14,11 +14,9 @@ export class FileUtil {
return extensions;
}
static toDataUrl(blob: Blob): Promise<string | ArrayBuffer> {
static fromBlobToDataUrl(blob: Blob): Promise<string | ArrayBuffer> {
return new Promise<string | ArrayBuffer>((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<Buffer> {
return new Promise<Buffer>((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<string> {
return new Promise<string>((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);
});
}
}

View File

@ -25,12 +25,20 @@ export class BrowserNativeService implements NativeService {
showImageViewer(): void {}
readFile(path: string): Observable<ArrayBuffer> {
return this.httpClient.get(path, { responseType: 'arraybuffer' });
readFile(path: string): Observable<Buffer> {
return this.httpClient.get(path, { responseType: 'arraybuffer' }).pipe(
map(arrayBuffer => {
return Buffer.from(arrayBuffer);
})
);
}
saveFile(path: string, buf: ArrayBuffer): Observable<boolean> {
return this.httpClient.post<boolean>(path, buf, {});
saveFile(
buffer: Buffer,
fileName: string,
path?: string
): Observable<string> {
return this.httpClient.post<string>(path, buffer, {});
}
windowStateChanged(): Observable<WindowState> {

View File

@ -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'));
})
);
}

View File

@ -42,8 +42,8 @@ export class ElectronNativeService implements NativeService {
ipcRenderer.send(Channel.showImageViewer);
}
readFile(path: string): Observable<ArrayBuffer> {
return new Observable<ArrayBuffer>(subscriber => {
readFile(path: string): Observable<Buffer> {
return new Observable<Buffer>(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<boolean> {
return new Observable<boolean>(subscriber => {
saveFile(
buffer: Buffer,
fileName: string,
path?: string
): Observable<string> {
return new Observable<string>(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 {

View File

@ -9,8 +9,8 @@ export interface NativeService {
showImageViewer(): void;
saveFile(path: string, buf: ArrayBuffer): Observable<boolean>;
readFile(path: string): Observable<ArrayBuffer>;
saveFile(buffer: Buffer, fileName: string, path?: string): Observable<string>;
readFile(path: string): Observable<Buffer>;
windowStateChanged(): Observable<WindowState>;
windowClose(): void;

View File

@ -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();