Fixed some minor transfer bugs as well added some more features required for the native clients file transfer
parent
52d998000d
commit
5af68c0a1c
|
@ -526,6 +526,10 @@ export class FileManager {
|
|||
|
||||
const initializeCallback = async () => {
|
||||
try {
|
||||
transfer.target = await transfer.targetSupplier(transfer);
|
||||
if(!transfer.target)
|
||||
throw tr("Failed to create transfer target");
|
||||
|
||||
await this.connectionHandler.serverConnection.send_command("ftinitdownload", {
|
||||
"path": options.path,
|
||||
"name": options.name,
|
||||
|
|
|
@ -47,7 +47,8 @@ export type TransferSourceSupplier = (transfer: FileUploadTransfer) => Promise<T
|
|||
/* Transfer target types */
|
||||
export enum TransferTargetType {
|
||||
RESPONSE,
|
||||
DOWNLOAD
|
||||
DOWNLOAD,
|
||||
FILE
|
||||
}
|
||||
|
||||
export abstract class TransferTarget {
|
||||
|
@ -72,6 +73,18 @@ export abstract class ResponseTransferTarget extends TransferTarget {
|
|||
abstract hasResponse() : boolean;
|
||||
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 enum FileTransferState {
|
||||
|
@ -134,7 +147,7 @@ export interface TransferInitializeError {
|
|||
export interface TransferConnectError {
|
||||
error: "connection";
|
||||
|
||||
reason: "missing-provider" | "provider-initialize-error" | "network-error";
|
||||
reason: "missing-provider" | "provider-initialize-error" | "handle-initialize-error" | "network-error";
|
||||
extraMessage?: string;
|
||||
}
|
||||
|
||||
|
@ -410,6 +423,7 @@ export abstract class TransferProvider {
|
|||
|
||||
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 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 createTextSource(text: string) : Promise<TextTransferSource> { throw tr("text source isn't supported"); };
|
||||
|
|
|
@ -219,7 +219,7 @@ export namespace CryptoHelper {
|
|||
}
|
||||
}
|
||||
|
||||
class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||
export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
|
||||
identity: TeaSpeakIdentity;
|
||||
handler: HandshakeCommandHandler<TeaSpeakHandshakeHandler>;
|
||||
|
||||
|
|
|
@ -360,6 +360,12 @@ export class Settings extends StaticSettings {
|
|||
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 => {
|
||||
return {
|
||||
key: 'invite_link_setting_' + name
|
||||
|
|
|
@ -426,7 +426,7 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
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"}>{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)
|
||||
return;
|
||||
|
||||
console.error(props.file.transfer);
|
||||
setTransferStatus("pending");
|
||||
});
|
||||
|
||||
|
@ -456,7 +455,6 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
|
|||
if(event.id !== props.file.transfer?.id)
|
||||
return;
|
||||
|
||||
console.error("Progress: " + event.progress);
|
||||
setTransferProgress(event.progress);
|
||||
setTransferStatus(event.status);
|
||||
});
|
||||
|
@ -609,43 +607,11 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
|
|||
event.preventDefault();
|
||||
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)}
|
||||
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} />
|
||||
</TableRowElement>
|
||||
|
@ -772,6 +738,23 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
|
||||
onHeaderContextMenu={e => this.onHeaderContextMenu(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} />}
|
||||
/>
|
||||
|
@ -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) {
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -1038,7 +1061,7 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
}
|
||||
} else {
|
||||
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 */
|
||||
element.size = event.fileSize;
|
||||
element.mode = "normal";
|
||||
|
|
|
@ -4,10 +4,27 @@
|
|||
.container {
|
||||
padding: 1em;
|
||||
position: relative;
|
||||
|
||||
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 {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
.containerIcon {
|
||||
margin: auto .25em;
|
||||
padding: .2em;
|
||||
|
@ -79,11 +96,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.fileTable {
|
||||
min-height: 5em;
|
||||
max-height: 40em;
|
||||
height: 400px;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
|
|
|
@ -212,7 +212,7 @@ class FileTransferModal extends Modal {
|
|||
|
||||
renderBody() {
|
||||
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} />
|
||||
<FileBrowser events={this.remoteBrowseEvents} currentPath={path} />
|
||||
<TransferInfo events={this.transferInfoEvents} />
|
||||
|
|
|
@ -10,14 +10,23 @@ import * as ppt from "tc-backend/ppt";
|
|||
import {SpecialKey} from "tc-shared/PPTListener";
|
||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||
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 {
|
||||
avatarsPathPrefix,
|
||||
channelPathPrefix,
|
||||
FileBrowserEvents,
|
||||
iconPathPrefix, ListedFileInfo, PathInfo
|
||||
iconPathPrefix,
|
||||
ListedFileInfo,
|
||||
PathInfo
|
||||
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
||||
function parsePath(path: string, connection: ConnectionHandler) : PathInfo {
|
||||
if(path === "/" || !path) {
|
||||
|
@ -649,6 +658,17 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
events.on("action_start_download", event => {
|
||||
event.files.forEach(file => {
|
||||
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 info = parsePath(file.path, connection);
|
||||
const transfer = connection.fileManager.initializeFileDownload({
|
||||
|
@ -656,7 +676,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
path: info.type === "channel" ? info.path : "",
|
||||
name: info.type === "channel" ? file.name : "/" + file.name,
|
||||
channelPassword: info.channel?.cached_password(),
|
||||
targetSupplier: async () => TransferProvider.provider().createDownloadTarget()
|
||||
targetSupplier: targetSupplier
|
||||
});
|
||||
transfer.awaitFinished().then(() => {
|
||||
if(transfer.transferState() === FileTransferState.ERRORED) {
|
||||
|
@ -736,12 +756,12 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
break;
|
||||
|
||||
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;
|
||||
|
||||
case FileTransferState.FINISHED:
|
||||
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;
|
||||
|
||||
case FileTransferState.ERRORED:
|
||||
|
|
|
@ -135,7 +135,8 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ export interface TableProperties {
|
|||
|
||||
onHeaderContextMenu?: (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>;
|
||||
}
|
||||
|
@ -140,7 +142,10 @@ export class Table extends React.Component<TableProperties, TableState> {
|
|||
}
|
||||
|
||||
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
|
||||
ref={this.refHeader}
|
||||
className={cssStyle.header + " " + (this.props.headerClassName || " ")}
|
||||
|
|
|
@ -19,7 +19,7 @@ debugger;
|
|||
debugger;
|
||||
const zzz = true ? "yyy" : "bbb";
|
||||
|
||||
const y = "";
|
||||
const zy = "";
|
||||
debugger;
|
||||
debugger;
|
||||
debugger;
|
||||
|
|
|
@ -60,16 +60,15 @@ TransferProvider.setProvider(new class extends TransferProvider {
|
|||
}
|
||||
|
||||
executeFileDownload(transfer: FileDownloadTransfer) {
|
||||
transfer.targetSupplier(transfer).then(target => {
|
||||
if(!target) throw tr("transfer target is undefined");
|
||||
transfer.target = target;
|
||||
try {
|
||||
if(!transfer.target) throw tr("transfer target is undefined");
|
||||
|
||||
let response: Promise<void>;
|
||||
transfer.setTransferState(FileTransferState.CONNECTING);
|
||||
if(target instanceof ResponseTransferTargetImpl) {
|
||||
response = responseFileDownload(transfer, target);
|
||||
} else if(target instanceof DownloadTransferTargetImpl) {
|
||||
response = downloadFileDownload(transfer, target);
|
||||
if(transfer.target instanceof ResponseTransferTargetImpl) {
|
||||
response = responseFileDownload(transfer, transfer.target);
|
||||
} else if(transfer.target instanceof DownloadTransferTargetImpl) {
|
||||
response = downloadFileDownload(transfer, transfer.target);
|
||||
} else {
|
||||
transfer.setFailed({
|
||||
error: "io",
|
||||
|
@ -93,7 +92,7 @@ TransferProvider.setProvider(new class extends TransferProvider {
|
|||
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||
});
|
||||
}).catch(error => {
|
||||
} catch (error) {
|
||||
if(typeof error !== "string")
|
||||
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",
|
||||
extraMessage: typeof error === "string" ? error : tr("Lookup the console")
|
||||
}, typeof error === "string" ? error : tr("Lookup the console"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
targetSupported(type: TransferTargetType) {
|
||||
|
|
Loading…
Reference in New Issue