412 lines
13 KiB
TypeScript
412 lines
13 KiB
TypeScript
import {
|
|
BrowserFileTransferSource,
|
|
BufferTransferSource,
|
|
FileDownloadTransfer, FileTransfer,
|
|
FileTransferState, FileTransferTarget,
|
|
FileUploadTransfer,
|
|
ResponseTransferTarget,
|
|
TextTransferSource,
|
|
TransferProvider,
|
|
TransferSourceType,
|
|
TransferTargetType
|
|
} from "tc-shared/file/Transfer";
|
|
import * as native from "tc-native/connection";
|
|
import {tr} from "tc-shared/i18n/localize";
|
|
import {LogCategory, logError} from "tc-shared/log";
|
|
import {base64_encode_ab} from "tc-shared/utils/buffers";
|
|
import * as path from "path";
|
|
import * as electron from "electron";
|
|
|
|
const executeTransfer = (transfer: FileTransfer, object: native.ft.TransferObject, callbackFinished: () => void) => {
|
|
const address = transfer.transferProperties().addresses[0];
|
|
let ntransfer: native.ft.NativeFileTransfer;
|
|
try {
|
|
ntransfer = native.ft.spawn_connection({
|
|
client_transfer_id: transfer.clientTransferId,
|
|
server_transfer_id: -1,
|
|
object: object,
|
|
remote_address: address.serverAddress,
|
|
remote_port: address.serverPort,
|
|
transfer_key: transfer.transferProperties().transferKey
|
|
});
|
|
} catch (error) {
|
|
let message = typeof error === "object" ? 'message' in error ? error.message : typeof error === "string" ? error : undefined : undefined;
|
|
if(!message) {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to create file transfer handle: %o"), error);
|
|
}
|
|
|
|
transfer.setFailed({
|
|
error: "connection",
|
|
reason: "handle-initialize-error",
|
|
extraMessage: message ? message : tr("Lookup the console")
|
|
}, message ? message : tr("Lookup the console"));
|
|
return;
|
|
}
|
|
|
|
ntransfer.callback_start = () => {
|
|
if(!transfer.isFinished()) {
|
|
transfer.setTransferState(FileTransferState.RUNNING);
|
|
transfer.lastStateUpdate = Date.now();
|
|
}
|
|
};
|
|
|
|
ntransfer.callback_failed = error => {
|
|
if(transfer.isFinished()) {
|
|
return;
|
|
}
|
|
|
|
transfer.lastStateUpdate = Date.now();
|
|
transfer.setFailed({
|
|
error: "connection",
|
|
reason: "network-error",
|
|
extraMessage: error
|
|
}, error);
|
|
};
|
|
|
|
ntransfer.callback_finished = aborted => {
|
|
if(transfer.isFinished()) {
|
|
return;
|
|
}
|
|
|
|
callbackFinished();
|
|
transfer.setTransferState(aborted ? FileTransferState.CANCELED : FileTransferState.FINISHED);
|
|
transfer.lastStateUpdate = Date.now();
|
|
};
|
|
|
|
ntransfer.callback_progress = (current, max) => {
|
|
if(transfer.isFinished()) {
|
|
return;
|
|
}
|
|
|
|
const transferInfo = transfer.lastProgressInfo();
|
|
/* ATTENTION: transferInfo.timestamp | 0 does not work since 1591875114970 > 2^32 (1591875114970 | 0 => -1557751846) */
|
|
if(transferInfo && Date.now() - (typeof transferInfo.timestamp === "number" ? transferInfo.timestamp : 0) < 2000 && !(transferInfo as any).native_info) {
|
|
return;
|
|
}
|
|
|
|
transfer.updateProgress({
|
|
network_current_speed: 0,
|
|
network_average_speed: 0,
|
|
|
|
network_bytes_send: 0,
|
|
network_bytes_received: 0,
|
|
|
|
file_current_offset: current,
|
|
file_total_size: max,
|
|
file_bytes_transferred: current,
|
|
file_start_offset: 0,
|
|
timestamp: Date.now(),
|
|
|
|
native_info: true
|
|
} as any);
|
|
transfer.lastStateUpdate = Date.now();
|
|
};
|
|
|
|
try {
|
|
if(!ntransfer.start()) {
|
|
throw tr("failed to start transfer");
|
|
}
|
|
} catch (error) {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to start file transfer: %o"), error);
|
|
}
|
|
|
|
transfer.setFailed({
|
|
error: "connection",
|
|
reason: "network-error",
|
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
|
return;
|
|
}
|
|
};
|
|
|
|
TransferProvider.setProvider(new class extends TransferProvider {
|
|
executeFileDownload(transfer: FileDownloadTransfer) {
|
|
try {
|
|
if(!transfer.target) {
|
|
throw tr("transfer target is undefined");
|
|
}
|
|
transfer.setTransferState(FileTransferState.CONNECTING);
|
|
|
|
let nativeTarget: native.ft.FileTransferTarget;
|
|
if(transfer.target instanceof ResponseTransferTargetImpl) {
|
|
transfer.target.initialize(transfer.transferProperties().fileSize);
|
|
nativeTarget = transfer.target.nativeTarget;
|
|
} else if(transfer.target instanceof FileTransferTargetImpl) {
|
|
nativeTarget = transfer.target.getNativeTarget(transfer.properties.name, transfer.transferProperties().fileSize);
|
|
} else {
|
|
transfer.setFailed({
|
|
error: "io",
|
|
reason: "unsupported-target"
|
|
}, tr("invalid transfer target type"));
|
|
return;
|
|
}
|
|
|
|
executeTransfer(transfer, nativeTarget, () => {
|
|
if(transfer.target instanceof ResponseTransferTargetImpl) {
|
|
transfer.target.createResponseFromBuffer();
|
|
}
|
|
});
|
|
} catch (error) {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer target: %o"), error);
|
|
}
|
|
|
|
transfer.setFailed({
|
|
error: "io",
|
|
reason: "failed-to-initialize-target",
|
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
|
}
|
|
}
|
|
|
|
executeFileUpload(transfer: FileUploadTransfer) {
|
|
try {
|
|
if(!transfer.source) {
|
|
throw tr("transfer source is undefined");
|
|
}
|
|
|
|
let nativeSource: native.ft.FileTransferSource;
|
|
if(transfer.source instanceof BrowserFileTransferSourceImpl) {
|
|
nativeSource = transfer.source.getNativeSource();
|
|
} else if(transfer.source instanceof TextTransferSourceImpl) {
|
|
nativeSource = transfer.source.getNativeSource();
|
|
} else if(transfer.source instanceof BufferTransferSourceImpl) {
|
|
nativeSource = transfer.source.getNativeSource();
|
|
} else {
|
|
transfer.setFailed({
|
|
error: "io",
|
|
reason: "unsupported-target"
|
|
}, tr("invalid transfer target type"));
|
|
return;
|
|
}
|
|
|
|
executeTransfer(transfer, nativeSource, () => { });
|
|
} catch (error) {
|
|
if(typeof error !== "string") {
|
|
logError(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer source: %o"), error);
|
|
}
|
|
|
|
transfer.setFailed({
|
|
error: "io",
|
|
reason: "failed-to-initialize-target",
|
|
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
|
}, typeof error === "string" ? error : tr("Lookup the console"));
|
|
}
|
|
}
|
|
|
|
sourceSupported(type: TransferSourceType) {
|
|
switch (type) {
|
|
case TransferSourceType.TEXT:
|
|
case TransferSourceType.BUFFER:
|
|
case TransferSourceType.BROWSER_FILE:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
targetSupported(type: TransferTargetType) {
|
|
switch (type) {
|
|
case TransferTargetType.RESPONSE:
|
|
case TransferTargetType.FILE:
|
|
return true;
|
|
|
|
case TransferTargetType.DOWNLOAD:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async createTextSource(text: string): Promise<TextTransferSource> {
|
|
return new TextTransferSourceImpl(text);
|
|
}
|
|
|
|
async createBufferSource(buffer: ArrayBuffer): Promise<BufferTransferSource> {
|
|
return new BufferTransferSourceImpl(buffer);
|
|
}
|
|
|
|
async createBrowserFileSource(file: File): Promise<BrowserFileTransferSource> {
|
|
return new BrowserFileTransferSourceImpl(file);
|
|
}
|
|
|
|
async createResponseTarget(): Promise<ResponseTransferTarget> {
|
|
return new ResponseTransferTargetImpl();
|
|
}
|
|
|
|
async createFileTarget(path?: string, name?: string): Promise<FileTransferTarget> {
|
|
const target = new FileTransferTargetImpl(path, name);
|
|
await target.requestPath();
|
|
return target;
|
|
}
|
|
});
|
|
|
|
class TextTransferSourceImpl extends TextTransferSource {
|
|
private readonly text: string;
|
|
private buffer: ArrayBuffer;
|
|
private nativeSource: native.ft.FileTransferSource;
|
|
|
|
constructor(text: string) {
|
|
super();
|
|
this.text = text;
|
|
}
|
|
|
|
getText(): string {
|
|
return this.text;
|
|
}
|
|
|
|
|
|
async fileSize(): Promise<number> {
|
|
return this.getArrayBuffer().byteLength;
|
|
}
|
|
|
|
getArrayBuffer() : ArrayBuffer {
|
|
if(this.buffer) return this.buffer;
|
|
|
|
const encoder = new TextEncoder();
|
|
this.buffer = encoder.encode(this.text);
|
|
return this.buffer;
|
|
}
|
|
|
|
getNativeSource() {
|
|
if(this.nativeSource) return this.nativeSource;
|
|
|
|
this.nativeSource = native.ft.upload_transfer_object_from_buffer(this.getArrayBuffer());
|
|
return this.nativeSource;
|
|
}
|
|
}
|
|
|
|
class BufferTransferSourceImpl extends BufferTransferSource {
|
|
private readonly buffer: ArrayBuffer;
|
|
private nativeSource: native.ft.FileTransferSource;
|
|
|
|
constructor(buffer: ArrayBuffer) {
|
|
super();
|
|
this.buffer = buffer;
|
|
}
|
|
|
|
|
|
async fileSize(): Promise<number> {
|
|
return this.buffer.byteLength;
|
|
}
|
|
|
|
getNativeSource() {
|
|
if(this.nativeSource) return this.nativeSource;
|
|
|
|
this.nativeSource = native.ft.upload_transfer_object_from_buffer(this.buffer);
|
|
return this.nativeSource;
|
|
}
|
|
|
|
getBuffer(): ArrayBuffer {
|
|
return this.buffer;
|
|
}
|
|
}
|
|
|
|
class BrowserFileTransferSourceImpl extends BrowserFileTransferSource {
|
|
private readonly file: File;
|
|
private nativeSource: native.ft.FileTransferSource;
|
|
|
|
constructor(file: File) {
|
|
super();
|
|
this.file = file;
|
|
}
|
|
|
|
|
|
async fileSize(): Promise<number> {
|
|
return this.file.size;
|
|
}
|
|
|
|
getNativeSource() {
|
|
if(this.nativeSource) return this.nativeSource;
|
|
|
|
this.nativeSource = native.ft.upload_transfer_object_from_file(path.dirname(this.file.path), path.basename(this.file.path));
|
|
return this.nativeSource;
|
|
}
|
|
|
|
getFile(): File {
|
|
return this.file;
|
|
}
|
|
}
|
|
|
|
class ResponseTransferTargetImpl extends ResponseTransferTarget {
|
|
nativeTarget: native.ft.FileTransferTarget;
|
|
buffer: Uint8Array;
|
|
private response: Response;
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
initialize(bufferSize: number) {
|
|
this.buffer = new Uint8Array(bufferSize);
|
|
this.nativeTarget = native.ft.download_transfer_object_from_buffer(this.buffer.buffer);
|
|
}
|
|
|
|
getResponse(): Response {
|
|
return this.response;
|
|
}
|
|
|
|
hasResponse(): boolean {
|
|
return typeof this.response === "object";
|
|
}
|
|
|
|
createResponseFromBuffer() {
|
|
const buffer = this.buffer.buffer.slice(this.buffer.byteOffset, this.buffer.byteOffset + Math.min(64, this.buffer.byteLength));
|
|
this.response = new Response(this.buffer, {
|
|
status: 200,
|
|
statusText: "success",
|
|
headers: {
|
|
"X-media-bytes": base64_encode_ab(buffer)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class FileTransferTargetImpl extends FileTransferTarget {
|
|
private path: string;
|
|
private name: string;
|
|
|
|
constructor(path: string, name?: string) {
|
|
super();
|
|
|
|
this.path = path;
|
|
this.name = name;
|
|
}
|
|
|
|
async requestPath() {
|
|
if(typeof this.path === "string") {
|
|
return;
|
|
}
|
|
|
|
const result = await electron.remote.dialog.showSaveDialog({ defaultPath: this.name });
|
|
if(result.canceled) {
|
|
throw tr("download canceled");
|
|
}
|
|
|
|
if(!result.filePath) {
|
|
throw tr("invalid result path");
|
|
}
|
|
|
|
this.path = path.dirname(result.filePath);
|
|
this.name = path.basename(result.filePath);
|
|
}
|
|
|
|
getNativeTarget(fallbackName: string, expectedSize: number) {
|
|
this.name = this.name || fallbackName;
|
|
|
|
return native.ft.download_transfer_object_from_file(this.path, this.name, expectedSize);
|
|
}
|
|
|
|
getFilePath(): string {
|
|
return this.path;
|
|
}
|
|
|
|
getFileName(): string {
|
|
return this.name;
|
|
}
|
|
|
|
hasFileName(): boolean {
|
|
return typeof this.name === "string";
|
|
}
|
|
} |