Fixed some minor transfer bugs as well added some more features required for the native clients file transfer

canary
WolverinDEV 2020-06-10 22:44:50 +02:00
parent 52d998000d
commit 5af68c0a1c
12 changed files with 152 additions and 63 deletions

View File

@ -526,6 +526,10 @@ export class FileManager {
const initializeCallback = async () => { const initializeCallback = async () => {
try { try {
transfer.target = await transfer.targetSupplier(transfer);
if(!transfer.target)
throw tr("Failed to create transfer target");
await this.connectionHandler.serverConnection.send_command("ftinitdownload", { await this.connectionHandler.serverConnection.send_command("ftinitdownload", {
"path": options.path, "path": options.path,
"name": options.name, "name": options.name,

View File

@ -47,7 +47,8 @@ export type TransferSourceSupplier = (transfer: FileUploadTransfer) => Promise<T
/* Transfer target types */ /* Transfer target types */
export enum TransferTargetType { export enum TransferTargetType {
RESPONSE, RESPONSE,
DOWNLOAD DOWNLOAD,
FILE
} }
export abstract class TransferTarget { export abstract class TransferTarget {
@ -72,6 +73,18 @@ export abstract class ResponseTransferTarget extends TransferTarget {
abstract hasResponse() : boolean; abstract hasResponse() : boolean;
abstract getResponse() : Response; abstract getResponse() : Response;
} }
export abstract class FileTransferTarget extends TransferTarget {
protected constructor() {
super(TransferTargetType.FILE);
}
abstract getFilePath() : string;
abstract hasFileName() : boolean;
abstract getFileName() : string;
}
export type TransferTargetSupplier = (transfer: FileDownloadTransfer) => Promise<TransferTarget>; export type TransferTargetSupplier = (transfer: FileDownloadTransfer) => Promise<TransferTarget>;
export enum FileTransferState { export enum FileTransferState {
@ -134,7 +147,7 @@ export interface TransferInitializeError {
export interface TransferConnectError { export interface TransferConnectError {
error: "connection"; error: "connection";
reason: "missing-provider" | "provider-initialize-error" | "network-error"; reason: "missing-provider" | "provider-initialize-error" | "handle-initialize-error" | "network-error";
extraMessage?: string; extraMessage?: string;
} }
@ -410,6 +423,7 @@ export abstract class TransferProvider {
async createResponseTarget() : Promise<ResponseTransferTarget> { throw tr("response target isn't supported"); } async createResponseTarget() : Promise<ResponseTransferTarget> { throw tr("response target isn't supported"); }
async createDownloadTarget(filename?: string) : Promise<DownloadTransferTarget> { throw tr("download target isn't supported"); } async createDownloadTarget(filename?: string) : Promise<DownloadTransferTarget> { throw tr("download target isn't supported"); }
async createFileTarget(path?: string, filename?: string) : Promise<FileTransferTarget> { throw tr("file target isn't supported"); }
async createBufferSource(buffer: ArrayBuffer) : Promise<BufferTransferSource> { throw tr("buffer source isn't supported"); } async createBufferSource(buffer: ArrayBuffer) : Promise<BufferTransferSource> { throw tr("buffer source isn't supported"); }
async createTextSource(text: string) : Promise<TextTransferSource> { throw tr("text source isn't supported"); }; async createTextSource(text: string) : Promise<TextTransferSource> { throw tr("text source isn't supported"); };

View File

@ -219,7 +219,7 @@ export namespace CryptoHelper {
} }
} }
class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeaSpeakIdentity; identity: TeaSpeakIdentity;
handler: HandshakeCommandHandler<TeaSpeakHandshakeHandler>; handler: HandshakeCommandHandler<TeaSpeakHandshakeHandler>;

View File

@ -360,6 +360,12 @@ export class Settings extends StaticSettings {
description: "Show finished file transfers in the file transfer list" description: "Show finished file transfers in the file transfer list"
}; };
static readonly KEY_TRANSFER_DOWNLOAD_FOLDER: SettingsKey<string> = {
key: "transfer_download_folder",
description: "The download folder for the file transfer downloads",
/* default_value: <users download directory> */
};
static readonly FN_INVITE_LINK_SETTING: (name: string) => SettingsKey<string> = name => { static readonly FN_INVITE_LINK_SETTING: (name: string) => SettingsKey<string> = name => {
return { return {
key: 'invite_link_setting_' + name key: 'invite_link_setting_' + name

View File

@ -426,7 +426,7 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
setSize(event.fileSize); setSize(event.fileSize);
}); });
if(size < 0 && props.file.size < 0) if(size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined"))
return <a key={"size-invalid"}><Translatable>unknown</Translatable></a>; return <a key={"size-invalid"}><Translatable>unknown</Translatable></a>;
return <a key={"size"}>{network.format_bytes(size >= 0 ? size : props.file.size, { unit: "B", time: "", exact: false })}</a>; return <a key={"size"}>{network.format_bytes(size >= 0 ? size : props.file.size, { unit: "B", time: "", exact: false })}</a>;
}; };
@ -439,7 +439,6 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
if(event.path !== props.file.path || event.name !== props.file.name) if(event.path !== props.file.path || event.name !== props.file.name)
return; return;
console.error(props.file.transfer);
setTransferStatus("pending"); setTransferStatus("pending");
}); });
@ -456,7 +455,6 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
if(event.id !== props.file.transfer?.id) if(event.id !== props.file.transfer?.id)
return; return;
console.error("Progress: " + event.progress);
setTransferProgress(event.progress); setTransferProgress(event.progress);
setTransferStatus(event.status); setTransferStatus(event.status);
}); });
@ -609,43 +607,11 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
event.preventDefault(); event.preventDefault();
setDropHovered(true); setDropHovered(true);
}} }}
onDrop={event => {
const types = event.dataTransfer.types;
if(types.length !== 1)
return;
if(props.row.userData.type !== FileType.DIRECTORY) {
event.stopPropagation();
return;
}
if(types[0] === FileTransferUrlMediaType) {
/* TODO: If cross move upload! */
console.error(event.dataTransfer.getData(FileTransferUrlMediaType));
const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e));
for(const fileUrl of fileUrls) {
const name = fileUrl.split("/").last();
const oldPath = fileUrl.split("/").slice(0, -1).join("/") + "/";
props.events.fire("action_rename_file", {
newPath: props.row.userData.path + props.row.userData.name + "/",
oldPath: oldPath,
oldName: name,
newName: name
});
}
} else if(types[0] === "Files") {
props.events.fire("action_start_upload", { path: props.row.userData.path + props.row.userData.name, mode: "files", files: [...event.dataTransfer.files] });
} else {
log.warn(LogCategory.FILE_TRANSFER, tr("Received an unknown drop media type (%o)"), types);
event.preventDefault();
return;
}
event.preventDefault();
}}
onDragLeave={() => setDropHovered(false)} onDragLeave={() => setDropHovered(false)}
onDragEnd={() => props.events.fire("notify_drag_ended")} onDragEnd={() => props.events.fire("notify_drag_ended")}
x-drag-upload-path={props.row.userData.type === FileType.DIRECTORY ? props.row.userData.path + props.row.userData.name + "/" : undefined}
> >
<FileTransferIndicator events={props.events} file={props.row.userData} /> <FileTransferIndicator events={props.events} file={props.row.userData} />
</TableRowElement> </TableRowElement>
@ -772,6 +738,23 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
onHeaderContextMenu={e => this.onHeaderContextMenu(e)} onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
onBodyContextMenu={e => this.onBodyContextMenu(e)} onBodyContextMenu={e => this.onBodyContextMenu(e)}
onDrop={e => this.onDrop(e)}
onDragOver={event => {
const types = event.dataTransfer.types;
if(types.length !== 1)
return;
if(types[0] === FileTransferUrlMediaType) {
/* TODO: Detect if its remote move or internal move */
event.dataTransfer.effectAllowed = "move";
} else if(types[0] === "Files") {
event.dataTransfer.effectAllowed = "copy";
} else {
return;
}
event.preventDefault();
}}
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => <FileListEntry columns={columns} row={row} key={uniqueId} events={this.props.events} />} renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => <FileListEntry columns={columns} row={row} key={uniqueId} events={this.props.events} />}
/> />
@ -786,6 +769,46 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
}); });
} }
private onDrop(event: React.DragEvent) {
const types = event.dataTransfer.types;
if(types.length !== 1)
return;
event.stopPropagation();
let targetPath;
{
let currentTarget = event.target as HTMLElement;
while(currentTarget && !currentTarget.hasAttribute("x-drag-upload-path"))
currentTarget = currentTarget.parentElement;
targetPath = currentTarget?.getAttribute("x-drag-upload-path") || this.currentPath;
console.log("Target: %o %s", currentTarget, targetPath);
}
if(types[0] === FileTransferUrlMediaType) {
/* TODO: If cross move upload! */
console.error(event.dataTransfer.getData(FileTransferUrlMediaType));
const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e));
for(const fileUrl of fileUrls) {
const name = fileUrl.split("/").last();
const oldPath = fileUrl.split("/").slice(0, -1).join("/") + "/";
this.props.events.fire("action_rename_file", {
newPath: targetPath,
oldPath: oldPath,
oldName: name,
newName: name
});
}
} else if(types[0] === "Files") {
this.props.events.fire("action_start_upload", { path: targetPath, mode: "files", files: [...event.dataTransfer.files] });
} else {
log.warn(LogCategory.FILE_TRANSFER, tr("Received an unknown drop media type (%o)"), types);
event.preventDefault();
return;
}
event.preventDefault();
}
private onHeaderContextMenu(event: React.MouseEvent) { private onHeaderContextMenu(event: React.MouseEvent) {
event.preventDefault(); event.preventDefault();
@ -1038,7 +1061,7 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
} }
} else { } else {
element.transfer.status = event.status; element.transfer.status = event.status;
if(element.mode === "uploading") { if(element.mode === "uploading" && event.status === "finished") {
/* upload finished, the element rerenders already with the correct values */ /* upload finished, the element rerenders already with the correct values */
element.size = event.fileSize; element.size = event.fileSize;
element.mode = "normal"; element.mode = "normal";

View File

@ -4,10 +4,27 @@
.container { .container {
padding: 1em; padding: 1em;
position: relative; position: relative;
padding-bottom: 4em; /* for the transfer info */ padding-bottom: 4em; /* for the transfer info */
display: flex;
flex-direction: column;
justify-content: stretch;
flex-shrink: 1;
flex-grow: 1;
width: 90em;
min-width: 10em;
max-width: 100%;
height: 55em;
max-height: 100%;
min-height: 10em;
.navigation { .navigation {
flex-grow: 0;
flex-shrink: 0;
.containerIcon { .containerIcon {
margin: auto .25em; margin: auto .25em;
padding: .2em; padding: .2em;
@ -79,11 +96,11 @@
} }
} }
.fileTable { .fileTable {
min-height: 5em; min-height: 5em;
max-height: 40em;
height: 400px; flex-grow: 1;
flex-shrink: 1;
margin-top: 1em; margin-top: 1em;

View File

@ -212,7 +212,7 @@ class FileTransferModal extends Modal {
renderBody() { renderBody() {
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/"; const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
return <div className={cssStyle.container} style={{width: "600px"}}> return <div className={cssStyle.container}>
<NavigationBar events={this.remoteBrowseEvents} currentPath={path} /> <NavigationBar events={this.remoteBrowseEvents} currentPath={path} />
<FileBrowser events={this.remoteBrowseEvents} currentPath={path} /> <FileBrowser events={this.remoteBrowseEvents} currentPath={path} />
<TransferInfo events={this.transferInfoEvents} /> <TransferInfo events={this.transferInfoEvents} />

View File

@ -10,14 +10,23 @@ import * as ppt from "tc-backend/ppt";
import {SpecialKey} from "tc-shared/PPTListener"; import {SpecialKey} from "tc-shared/PPTListener";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {tra, traj} from "tc-shared/i18n/localize"; import {tra, traj} from "tc-shared/i18n/localize";
import {FileTransfer, FileTransferState, FileUploadTransfer, TransferProvider} from "tc-shared/file/Transfer"; import {
FileTransfer,
FileTransferState,
FileUploadTransfer,
TransferProvider,
TransferTargetType
} from "tc-shared/file/Transfer";
import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal} from "tc-shared/ui/elements/Modal";
import { import {
avatarsPathPrefix, avatarsPathPrefix,
channelPathPrefix, channelPathPrefix,
FileBrowserEvents, FileBrowserEvents,
iconPathPrefix, ListedFileInfo, PathInfo iconPathPrefix,
ListedFileInfo,
PathInfo
} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; } from "tc-shared/ui/modal/transfer/ModalFileTransfer";
import {Settings, settings} from "tc-shared/settings";
function parsePath(path: string, connection: ConnectionHandler) : PathInfo { function parsePath(path: string, connection: ConnectionHandler) : PathInfo {
if(path === "/" || !path) { if(path === "/" || !path) {
@ -649,6 +658,17 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
events.on("action_start_download", event => { events.on("action_start_download", event => {
event.files.forEach(file => { event.files.forEach(file => {
try { try {
let targetSupplier;
if(__build.target === "client" && TransferProvider.provider().targetSupported(TransferTargetType.FILE)) {
const target = TransferProvider.provider().createFileTarget(undefined, file.name);
targetSupplier = async () => target;
} else if(TransferProvider.provider().targetSupported(TransferTargetType.DOWNLOAD)) {
targetSupplier = async () => await TransferProvider.provider().createDownloadTarget();
} else {
createErrorModal(tr("Failed to create transfer target"), tr("Failed to create transfer target.\nAll targets are unsupported")).open();
return;
}
const fileName = file.name; const fileName = file.name;
const info = parsePath(file.path, connection); const info = parsePath(file.path, connection);
const transfer = connection.fileManager.initializeFileDownload({ const transfer = connection.fileManager.initializeFileDownload({
@ -656,7 +676,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
path: info.type === "channel" ? info.path : "", path: info.type === "channel" ? info.path : "",
name: info.type === "channel" ? file.name : "/" + file.name, name: info.type === "channel" ? file.name : "/" + file.name,
channelPassword: info.channel?.cached_password(), channelPassword: info.channel?.cached_password(),
targetSupplier: async () => TransferProvider.provider().createDownloadTarget() targetSupplier: targetSupplier
}); });
transfer.awaitFinished().then(() => { transfer.awaitFinished().then(() => {
if(transfer.transferState() === FileTransferState.ERRORED) { if(transfer.transferState() === FileTransferState.ERRORED) {
@ -736,12 +756,12 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
break; break;
case FileTransferState.RUNNING: case FileTransferState.RUNNING:
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "transferring", fileSize: transfer.transferProperties().fileSize }); events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "transferring" });
break; break;
case FileTransferState.FINISHED: case FileTransferState.FINISHED:
case FileTransferState.CANCELED: case FileTransferState.CANCELED:
events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "finished" }); events.fire("notify_transfer_status", { id: transfer.clientTransferId, status: "finished", fileSize: transfer.transferProperties().fileSize });
break; break;
case FileTransferState.ERRORED: case FileTransferState.ERRORED:

View File

@ -135,7 +135,8 @@
overflow-y: auto; overflow-y: auto;
overflow-x: auto; overflow-x: auto;
display: block; display: flex;
flex-direction: column;
} }
} }
} }

View File

@ -35,6 +35,8 @@ export interface TableProperties {
onHeaderContextMenu?: (event: React.MouseEvent) => void; onHeaderContextMenu?: (event: React.MouseEvent) => void;
onBodyContextMenu?: (event: React.MouseEvent) => void; onBodyContextMenu?: (event: React.MouseEvent) => void;
onDrop?: (event: React.DragEvent) => void;
onDragOver?: (event: React.DragEvent) => void;
renderRow?: (row: TableRow, columns: TableColumn[], uniqueId: string) => React.ReactElement<TableRowElement>; renderRow?: (row: TableRow, columns: TableColumn[], uniqueId: string) => React.ReactElement<TableRowElement>;
} }
@ -140,7 +142,10 @@ export class Table extends React.Component<TableProperties, TableState> {
} }
return ( return (
<div className={cssStyle.container + " " + (this.props.className || " ")}> <div
className={cssStyle.container + " " + (this.props.className || " ")}
onDrop={e => this.props.onDrop && this.props.onDrop(e)}
onDragOver={e => this.props.onDragOver && this.props.onDragOver(e)}>
<div <div
ref={this.refHeader} ref={this.refHeader}
className={cssStyle.header + " " + (this.props.headerClassName || " ")} className={cssStyle.header + " " + (this.props.headerClassName || " ")}

View File

@ -19,7 +19,7 @@ debugger;
debugger; debugger;
const zzz = true ? "yyy" : "bbb"; const zzz = true ? "yyy" : "bbb";
const y = ""; const zy = "";
debugger; debugger;
debugger; debugger;
debugger; debugger;

View File

@ -60,16 +60,15 @@ TransferProvider.setProvider(new class extends TransferProvider {
} }
executeFileDownload(transfer: FileDownloadTransfer) { executeFileDownload(transfer: FileDownloadTransfer) {
transfer.targetSupplier(transfer).then(target => { try {
if(!target) throw tr("transfer target is undefined"); if(!transfer.target) throw tr("transfer target is undefined");
transfer.target = target;
let response: Promise<void>; let response: Promise<void>;
transfer.setTransferState(FileTransferState.CONNECTING); transfer.setTransferState(FileTransferState.CONNECTING);
if(target instanceof ResponseTransferTargetImpl) { if(transfer.target instanceof ResponseTransferTargetImpl) {
response = responseFileDownload(transfer, target); response = responseFileDownload(transfer, transfer.target);
} else if(target instanceof DownloadTransferTargetImpl) { } else if(transfer.target instanceof DownloadTransferTargetImpl) {
response = downloadFileDownload(transfer, target); response = downloadFileDownload(transfer, transfer.target);
} else { } else {
transfer.setFailed({ transfer.setFailed({
error: "io", error: "io",
@ -93,7 +92,7 @@ TransferProvider.setProvider(new class extends TransferProvider {
extraMessage: typeof error === "string" ? error : tr("Lookup the console") extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console")); }, typeof error === "string" ? error : tr("Lookup the console"));
}); });
}).catch(error => { } catch (error) {
if(typeof error !== "string") if(typeof error !== "string")
log.error(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer target: %o"), error); log.error(LogCategory.FILE_TRANSFER, tr("Failed to initialize transfer target: %o"), error);
@ -102,7 +101,7 @@ TransferProvider.setProvider(new class extends TransferProvider {
reason: "failed-to-initialize-target", reason: "failed-to-initialize-target",
extraMessage: typeof error === "string" ? error : tr("Lookup the console") extraMessage: typeof error === "string" ? error : tr("Lookup the console")
}, typeof error === "string" ? error : tr("Lookup the console")); }, typeof error === "string" ? error : tr("Lookup the console"));
}); }
} }
targetSupported(type: TransferTargetType) { targetSupported(type: TransferTargetType) {