diff --git a/src/browser.ts b/src/browser.ts index 03ce1e1..ff8288f 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -185,6 +185,10 @@ class BrowserFileWriter implements TrzszFileWriter { this.closed = true; } } + + public async deleteFile() { + return ""; + } } export async function selectSaveDirectory(): Promise { diff --git a/src/comm.ts b/src/comm.ts index 1ec3081..1d7d8e0 100644 --- a/src/comm.ts +++ b/src/comm.ts @@ -104,6 +104,13 @@ export class TrzszError extends Error { return this.type === "fail" || this.type === "FAIL"; } + public isStopAndDelete() { + if (this.type !== "fail") { + return false; + } + return this.message === "Stopped and deleted"; + } + public static getErrorMessage(err: Error) { if (err instanceof TrzszError && !err.isTraceBack()) { return err.message; @@ -132,6 +139,7 @@ export interface TrzszFileWriter extends TrzszFile { getLocalName: () => string; isDir: () => boolean; writeFile: (buf: Uint8Array) => Promise; + deleteFile: () => Promise; } export type OpenSaveFile = ( @@ -283,6 +291,11 @@ async function setTmuxStatusInterval(interval: string) { await exec(`tmux setw status-interval ${interval}`); } +export async function tmuxRefreshClient() { + const exec = require("util").promisify(require("child_process").exec); + await exec("tmux refresh-client"); +} + export function getTerminalColumns() { return process.stdout.columns; } diff --git a/src/nodefs.ts b/src/nodefs.ts index fbf7e01..e92d2d8 100644 --- a/src/nodefs.ts +++ b/src/nodefs.ts @@ -17,7 +17,19 @@ function requireSafely(name: string) { } } -function promisify(fs: any, funcs: string[]) { +function promisify1(fs: any, funcs: string[]) { + for (const func of funcs) { + fs[func + "Async"] = (...args: any) => { + return new Promise((resolve) => { + fs[func](...args, (err: Error) => resolve(!err)); + }); + }; + } +} + +promisify1(fs, ["rm", "rmdir", "unlink", "access"]); + +function promisify2(fs: any, funcs: string[]) { for (const func of funcs) { fs[func + "Async"] = (...args: any) => { return new Promise((resolve, reject) => { @@ -33,11 +45,7 @@ function promisify(fs: any, funcs: string[]) { } } -promisify(fs, ["stat", "access", "mkdir", "readdir", "close", "open", "realpath", "write"]); - -async function fsExists(path: string): Promise { - return new Promise((resolve) => fs.access(path, (err: Error) => resolve(!err))); -} +promisify2(fs, ["stat", "mkdir", "readdir", "close", "open", "realpath", "write"]); async function fsRead( fd: number, @@ -62,16 +70,14 @@ export async function checkPathWritable(filePath: string) { return false; } - if (!(await fsExists(filePath))) { + if (!(await fs.accessAsync(filePath))) { throw new TrzszError(`No such directory: ${filePath}`); } const stats = await fs.statAsync(filePath); if (!stats.isDirectory()) { throw new TrzszError(`Not a directory: ${filePath}`); } - try { - await fs.accessAsync(filePath, fs.constants.W_OK); - } catch (err) { + if (!(await fs.accessAsync(filePath, fs.constants.W_OK))) { throw new TrzszError(`No permission to write: ${filePath}`); } @@ -145,9 +151,7 @@ async function checkPathReadable( if (!stats.isFile()) { throw new TrzszError(`Not a regular file: ${absPath}`); } - try { - await fs.accessAsync(absPath, fs.constants.R_OK); - } catch (err) { + if (!(await fs.accessAsync(absPath, fs.constants.R_OK))) { throw new TrzszError(`No permission to read: ${absPath}`); } fileList.push(new NodefsFileReader(pathId, absPath, relPath, false, stats.size)); @@ -179,7 +183,7 @@ export async function checkPathsReadable( const entries = filePaths.entries(); for (const [idx, filePath] of entries) { const absPath = path.resolve(filePath); - if (!(await fsExists(absPath))) { + if (!(await fs.accessAsync(absPath))) { throw new TrzszError(`No such file: ${absPath}`); } const stats = await fs.statAsync(absPath); @@ -193,13 +197,15 @@ export async function checkPathsReadable( } class NodefsFileWriter implements TrzszFileWriter { + private absPath: string; private fileName: string; private localName: string; private fd: number | null; private dir: boolean; private closed: boolean = false; - constructor(fileName: string, localName: string, fd: number | null, dir: boolean = false) { + constructor(absPath: string, fileName: string, localName: string, fd: number | null, dir: boolean = false) { + this.absPath = absPath; this.fileName = fileName; this.localName = localName; this.fd = fd; @@ -231,15 +237,42 @@ class NodefsFileWriter implements TrzszFileWriter { } } } + + public async deleteFile() { + if (!this.absPath || !(await fs.accessAsync(this.absPath))) { + return ""; + } + try { + await this.closeFile(); + if (typeof fs.rm === "function") { + if (await fs.rmAsync(this.absPath, { recursive: true })) { + return this.absPath; + } + } else { + if (this.isDir) { + if (await fs.rmdirAsync(this.absPath, { recursive: true })) { + return this.absPath; + } + } else { + if (await fs.unlinkAsync(this.absPath)) { + return this.absPath; + } + } + } + } catch (err) { + console.log(`delete [${this.absPath}] failed`, err); + } + return ""; + } } async function getNewName(savePath: string, fileName: string) { - if (!(await fsExists(path.join(savePath, fileName)))) { + if (!(await fs.accessAsync(path.join(savePath, fileName)))) { return fileName; } for (let i = 0; i < 1000; i++) { const saveName = `${fileName}.${i}`; - if (!(await fsExists(path.join(savePath, saveName)))) { + if (!(await fs.accessAsync(path.join(savePath, saveName)))) { return saveName; } } @@ -260,19 +293,22 @@ async function doCreateFile(absPath: string) { } async function doCreateDirectory(absPath: string) { - if (!(await fsExists(absPath))) { + if (!(await fs.accessAsync(absPath))) { await fs.mkdirAsync(absPath, { recursive: true, mode: 0o755 }); + return true; } const stats = await fs.statAsync(absPath); if (!stats.isDirectory()) { throw new TrzszError(`Not a directory: ${absPath}`); } + return false; } async function createFile(savePath: string, fileName: string, overwrite: boolean) { const localName = overwrite ? fileName : await getNewName(savePath, fileName); - const fd = await doCreateFile(path.join(savePath, localName)); - return new NodefsFileWriter(fileName, localName, fd); + const absPath = path.join(savePath, localName); + const fd = await doCreateFile(absPath); + return new NodefsFileWriter(absPath, fileName, localName, fd); } export async function openSaveFile(saveParam: any, fileName: string, directory: boolean, overwrite: boolean) { @@ -313,10 +349,13 @@ export async function openSaveFile(saveParam: any, fileName: string, directory: } if (file.is_dir === true) { - await doCreateDirectory(fullPath); - return new NodefsFileWriter(fileName, localName, null, true); + let absPath = ""; + if (await doCreateDirectory(fullPath)) { + absPath = fullPath; + } + return new NodefsFileWriter(absPath, fileName, localName, null, true); } const fd = await doCreateFile(fullPath); - return new NodefsFileWriter(fileName, localName, fd); + return new NodefsFileWriter(fullPath, fileName, localName, fd); } diff --git a/src/transfer.ts b/src/transfer.ts index 3b8525b..3f7d530 100644 --- a/src/transfer.ts +++ b/src/transfer.ts @@ -21,6 +21,7 @@ import { TrzszFileWriter, ProgressCallback, stripTmuxStatusLine, + tmuxRefreshClient, } from "./comm"; /* eslint-disable require-jsdoc */ @@ -32,6 +33,7 @@ export class TrzszTransfer { private remoteIsWindows: boolean = false; private lastInputTime: number = 0; private openedFiles: TrzszFile[] = []; + private createdFiles: TrzszFileWriter[] = []; private tmuxOutputJunk: boolean = false; private cleanTimeoutInMilliseconds: number = 100; private transferConfig: any = {}; @@ -161,7 +163,7 @@ export class TrzszTransfer { return uint8ToStr(decodeBuffer(buf), "utf8"); } - private async checkString(expect: string) { + protected async checkString(expect: string) { const result = await this.recvString("SUCC"); if (result !== expect) { throw new TrzszError(`String check [${result}] <> [${expect}]`, null, true); @@ -307,6 +309,20 @@ export class TrzszTransfer { } process.stdout.write(msg); process.stdout.write("\r\n"); + if (this.transferConfig.tmux_output_junk) { + await tmuxRefreshClient(); + } + } + + private async deleteCreatedFiles() { + const deletedFiles: string[] = []; + for (const file of this.createdFiles) { + const path = await file.deleteFile(); + if (path) { + deletedFiles.push(path); + } + } + return deletedFiles; } public async clientError(err: Error) { @@ -339,6 +355,13 @@ export class TrzszTransfer { const errMsg = TrzszError.getErrorMessage(err); let trace = true; if (err instanceof TrzszError) { + if (err.isStopAndDelete()) { + const deletedFiles = await this.deleteCreatedFiles(); + if (deletedFiles && deletedFiles.length) { + await this.serverExit([err.message + ":"].concat(deletedFiles).join("\r\n- ")); + return; + } + } trace = err.isTraceBack(); if (err.isRemoteExit() || err.isRemoteFail()) { await this.serverExit(errMsg); @@ -486,6 +509,7 @@ export class TrzszTransfer { ) { const fileName = await this.recvString("NAME"); const file = await openSaveFile(saveParam, fileName, directory, overwrite); + this.createdFiles.push(file); await this.sendString("SUCC", file.getLocalName()); if (progressCallback) { progressCallback.onName(file.getFileName());