diff --git a/src/components/Downloads.tsx b/src/components/Downloads.tsx index 4ca38760..f28e6b47 100644 --- a/src/components/Downloads.tsx +++ b/src/components/Downloads.tsx @@ -12,6 +12,7 @@ import classnames from 'classnames'; import { intentClass } from '@blueprintjs/core/lib/esm/common/classes'; import { AppAlert } from './AppAlert'; import CONFIG from '../config/appConfig' +import { isWin } from '../utils/platform'; const TYPE_ICONS: { [key: string]: IconName } = { 'img': 'media', @@ -244,7 +245,7 @@ class DownloadsClass extends React.Component { const intent = this.getIntent(transfer); const className = intentClass(intent); - + const sep = isWin ? '\\' : '/'; const node: ITreeNode = { id: transfer.id, @@ -265,7 +266,7 @@ class DownloadsClass extends React.Component { node.childNodes.push({ id: id, icon: file.file.isDir ? 'folder-close' : this.getFileIcon(filetype), - label: file.subDirectory ? (file.subDirectory + '/' + file.file.fullname) : file.file.fullname, + label: file.subDirectory ? (file.subDirectory + sep + file.file.fullname) : file.file.fullname, secondaryLabel: this.createFileRightLabel(file), nodeData: { transfer: file, diff --git a/src/components/SideView.tsx b/src/components/SideView.tsx index 33d2e72c..1b7339d8 100644 --- a/src/components/SideView.tsx +++ b/src/components/SideView.tsx @@ -4,20 +4,17 @@ import { Toolbar } from './Toolbar'; import { Statusbar } from './Statusbar'; import { AppState } from "../state/appState"; import { LoginDialog } from "./dialogs/LoginDialog"; -import { FileState } from "../state/fileState"; import { Loader } from "./Loader"; import { FileTable } from "./FileTable"; import classnames from 'classnames'; import { DropTargetSpec, DropTargetConnector, DropTargetMonitor, DropTargetCollector, ConnectDropTarget, DropTarget } from "react-dnd"; -import { DraggedObject } from './RowRenderer'; import { TabList } from "./TabList"; import { ViewState } from "../state/viewState"; import { AppToaster } from "./AppToaster"; import { Intent } from "@blueprintjs/core"; import { getLocalizedError } from "../locale/error"; import { withNamespaces, WithNamespaces } from "react-i18next"; -import { FsLocal, LocalApi } from "../services/plugins/FsLocal"; -import { getFS } from "../services/Fs"; +import { LocalApi } from "../services/plugins/FsLocal"; interface SideViewProps extends WithNamespaces { hide: boolean; diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 6fac1f76..5040fbb4 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -7,7 +7,7 @@ import { FileMenu } from "./FileMenu"; import { MakedirDialog } from "./dialogs/MakedirDialog"; import { AppAlert } from './AppAlert'; import { Logger } from "./Log"; -import { AppToaster, IToasterOpts } from "./AppToaster"; +import { AppToaster } from "./AppToaster"; import { withNamespaces, WithNamespaces, Trans } from "react-i18next"; import { WithMenuAccelerators, Accelerators, Accelerator } from "./WithMenuAccelerators"; import { throttle } from "../utils/throttle"; @@ -206,8 +206,9 @@ export class ToolbarClass extends React.Component { try { const { viewState } = this.injected; const fileCache = viewState.getVisibleCache(); + const cache = this.cache; - const deleted = await this.cache.delete(this.state.path, fileCache.selected); + const deleted = await cache.delete(this.state.path, fileCache.selected); // TODO: reset selection ? if (!deleted) { @@ -217,7 +218,9 @@ export class ToolbarClass extends React.Component { // show warning this.onDeleteError(); } - this.cache.reload(); + if (cache.getFS().options.needsRefresh) { + cache.reload(); + } } } catch (err) { this.onDeleteError(err); diff --git a/src/services/Fs.ts b/src/services/Fs.ts index 09e3135f..4b3a0b24 100644 --- a/src/services/Fs.ts +++ b/src/services/Fs.ts @@ -92,7 +92,7 @@ export function MakeId(stats: any): FileID { } function isModeExe(mode: number, gid: number, uid: number): Boolean { - if (isWin || mode === -1) { + if (isWin || mode === -1) { return false; } @@ -125,7 +125,7 @@ export function filetype(mode: number, gid: number, uid: number, extension: stri export interface FsApi { // public API // async methods that may require server access - list(dir: string, transferId?: number): Promise; + list(dir: string, watchDir?: boolean, transferId?: number): Promise; cd(path: string, transferId?: number): Promise; delete(parent: string, files: File[], transferId?: number): Promise; makedir(parent: string, name: string, transferId?: number): Promise; diff --git a/src/services/plugins/FsGeneric.ts b/src/services/plugins/FsGeneric.ts index 3d0398b7..15395c85 100644 --- a/src/services/plugins/FsGeneric.ts +++ b/src/services/plugins/FsGeneric.ts @@ -86,7 +86,7 @@ class GenericApi implements FsApi { } as File); } - async list(dir: string, transferId = -1): Promise { + async list(dir: string, watchDir = false, transferId = -1): Promise { console.log("FsGeneric.readDirectory"); const pathExists = await this.isDir(dir); @@ -97,7 +97,7 @@ class GenericApi implements FsApi { } } - off() {} + off() { } isRoot(path: string): boolean { return path === "/"; @@ -129,7 +129,7 @@ class GenericApi implements FsApi { getParentTree(dir: string): Array<{ dir: string; fullname: string }> { const numParts = dir.replace(/^\//, "").split("/").length; const folders = []; - for (let i = 0; i < numParts; ++i) {} + for (let i = 0; i < numParts; ++i) { } return []; } @@ -137,7 +137,7 @@ class GenericApi implements FsApi { return path; } - on(event: string, cb: (data: any) => void): void {} + on(event: string, cb: (data: any) => void): void { } } export const FsGeneric: Fs = { diff --git a/src/services/plugins/FsLocal.ts b/src/services/plugins/FsLocal.ts index 7a76c93d..4b07fbc4 100644 --- a/src/services/plugins/FsLocal.ts +++ b/src/services/plugins/FsLocal.ts @@ -30,7 +30,7 @@ export class LocalApi implements FsApi { loginOptions: ICredentials = null; onFsChange: (filename: string) => void; - constructor(path: string, onFsChange: (filename: string) => void) { + constructor(_: string, onFsChange: (filename: string) => void) { this.path = ""; this.onFsChange = onFsChange; } @@ -151,14 +151,14 @@ export class LocalApi implements FsApi { }); } }) - .catch((err) => { - reject({ - code: err.code, - message: err.message, - newName: newName, - oldName: file.fullname - }); - }) + .catch((err) => { + reject({ + code: err.code, + message: err.message, + newName: newName, + oldName: file.fullname + }); + }) }); } else { // reject promise with previous name in case of invalid chars @@ -249,7 +249,7 @@ export class LocalApi implements FsApi { } } - async list(dir: string, transferId = -1): Promise { + async list(dir: string, watchDir = false, transferId = -1): Promise { try { await this.isDir(dir); return new Promise((resolve, reject) => { @@ -258,24 +258,24 @@ export class LocalApi implements FsApi { reject(err); } else { const dirPath = path.resolve(dir); - + const files: File[] = []; - + for (var i = 0; i < items.length; i++) { const file = LocalApi.fileFromPath( path.join(dirPath, items[i]) ); files.push(file); } - - this.onList(dirPath); - + + watchDir && this.onList(dirPath); + resolve(files); } }); }); - } catch(err) { - throw({ + } catch (err) { + throw ({ code: err.code, message: `Could not access path: ${dir}` }); @@ -310,7 +310,7 @@ export class LocalApi implements FsApi { ctime: new Date(), mtime: new Date(), birthtime: new Date(), - size: stats.size, + size: stats ? stats.size : 0, isDirectory: () => isDir, mode: -1, isSymbolicLink: () => isSymLink, @@ -343,7 +343,7 @@ export class LocalApi implements FsApi { filetype(mode, 0, 0, extension)) || "", isSym: stats.isSymbolicLink(), - target: stats.isSymbolicLink() && name || null, + target: stats.isSymbolicLink() && name || null, id: MakeId(stats) }; @@ -486,7 +486,7 @@ export class LocalApi implements FsApi { return isWin ? (path.match(/\\$/) ? path : path + "\\") : path; } - on(event: string, cb: (data: any) => void): void {} + on(event: string, cb: (data: any) => void): void { } } export function FolderExists(path: string) { diff --git a/src/services/plugins/FsSimpleFtp.ts b/src/services/plugins/FsSimpleFtp.ts index bc2af731..565e5153 100644 --- a/src/services/plugins/FsSimpleFtp.ts +++ b/src/services/plugins/FsSimpleFtp.ts @@ -345,7 +345,7 @@ class SimpleFtpApi implements FsApi { } as File); } - list(path: string, transferId = -1): Promise { + list(path: string, watchDir = false, transferId = -1): Promise { return new Promise(async (resolve, reject) => { const newpath = this.pathpart(path); diff --git a/src/services/plugins/LocalWatch.ts b/src/services/plugins/LocalWatch.ts index ae647ce6..ba6c72b6 100644 --- a/src/services/plugins/LocalWatch.ts +++ b/src/services/plugins/LocalWatch.ts @@ -31,7 +31,7 @@ export const LocalWatch = { }, createCallback(path: string) { // console.log('LocalWatch.createCallback', path); - return debounce((eventType: string, filename: string | Buffer) => { + return debounce((_: string, filename: string) => { // console.log('changeCallback', eventType, filename); const callbacks = this.getCallbacks(path); for (let cb of callbacks) { diff --git a/src/services/plugins/__tests__/FsLocal.test.ts b/src/services/plugins/__tests__/FsLocal.test.ts index 255a31f3..b2ae08d0 100644 --- a/src/services/plugins/__tests__/FsLocal.test.ts +++ b/src/services/plugins/__tests__/FsLocal.test.ts @@ -1,9 +1,11 @@ import { FsLocal } from "../FsLocal"; import * as fs from 'fs'; +import { sep } from 'path'; import { describeUnix, describeWin, + itUnix, getPath, } from "../../../utils/test/helpers"; @@ -21,122 +23,122 @@ const TESTS_DIR = getPath("tests_dir"); console.log(''); describe('FsLocal', () => { - describeUnix("isDirectoryNameValid", function() { + describeUnix("isDirectoryNameValid", function () { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); }); - - it("unix: dir name cannot start with ../", function() { + + it("unix: dir name cannot start with ../", function () { const isValid = localAPI.isDirectoryNameValid("../"); - + expect(isValid).toBe(false); }); - - it("unix: dir name cannot start with ./", function() { + + it("unix: dir name cannot start with ./", function () { const isValid = localAPI.isDirectoryNameValid("./"); - + expect(isValid).toBe(false); }); - - it("unix: dir name cannot start with ../.", function() { + + it("unix: dir name cannot start with ../.", function () { const isValid = localAPI.isDirectoryNameValid("../."); - + expect(isValid).toBe(false); }); }); - + // @todo add Windows specific tests for isDirectoryNameValid - describe("resolve", function() { + describe("resolve", function () { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); }); - - it("~ resolves to homedir", function() { + + it("~ resolves to homedir", function () { const dir = localAPI.resolve("~"); - + expect(dir).toBe(HOMEDIR); }); }); - - describe("cd", function() { + + describe("cd", function () { const filePath = `${TESTS_DIR}/file` beforeAll(() => { mock(TmpDir); - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); }); afterAll(() => mock.restore()); - - it("should throw ENOTDIR if is not a directory", async function() { + + it("should throw ENOTDIR if is not a directory", async function () { expect.assertions(1); - + try { await localAPI.cd(filePath); } catch (err) { expect(err.code).toBe("ENOTDIR"); } }); - - it("should throw if does not exist", async function() { + + it("should throw if does not exist", async function () { expect.assertions(1); - + try { await localAPI.cd(`${filePath}__`); } catch (err) { expect(err.code).toBe("ENOENT"); } }); - - it("should return resolved path if exists and is a dir", function() { + + it("should return resolved path if exists and is a dir", function () { const dir = localAPI.resolve(TESTS_DIR); - + expect(dir).toBe(TESTS_DIR); }); - - it("should return resolved homedir path for ~", function() { + + it("should return resolved homedir path for ~", function () { expect.assertions(1); - + const dir = localAPI.resolve("~"); - + expect(dir).toBe(HOMEDIR); }); }); - - describe("size", function() { + + describe("size", function () { const SIZE_PATH = `${TESTS_DIR}/sizeTest`; - + beforeAll(() => { mock(TmpDir); - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); }); afterAll(() => mock.restore()); - + it("should return 0 if no files", async () => { expect.assertions(1); - + const size = await localAPI.size(__dirname, []); expect(size).toBe(0); }); - + it("should return correct size for 14 bytes file", async () => { expect.assertions(1); - + const size = await localAPI.size(SIZE_PATH, ["14bytes"]); expect(size).toBe(14); }); - + it("should return correct size for several 14 + 1024 files", async () => { expect.assertions(1); - + const size = await localAPI.size(SIZE_PATH, ["14bytes", "1024bytes"]); expect(size).toBe(1038); }); - + it("should throw ENOENT if file does not exist", async () => { expect.assertions(1); - + try { await localAPI.size(SIZE_PATH, ["15bytes"]); } catch (err) { @@ -144,20 +146,20 @@ describe('FsLocal', () => { } }); }); - - describe("makedir", function() { - const MAKEDIR_PATH = `${TESTS_DIR}/makedirTest`; - + + describe("makedir", function () { + const MAKEDIR_PATH = `${TESTS_DIR}${sep}makedirTest`; + beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); - + afterAll(() => mock.restore()); it("should return success if folder already exists", async () => { expect.assertions(1); - + const resolved = await localAPI.makedir(MAKEDIR_PATH, ""); expect(resolved).toBe(MAKEDIR_PATH); }); @@ -173,39 +175,39 @@ describe('FsLocal', () => { // expect(err.code).toMatch(/EACCES|EROFS/); // } // }); - + it("should throw EACCES if source is not readable", async () => { expect.assertions(1); - + try { await localAPI.makedir(`${MAKEDIR_PATH}/denied`, "foo"); } catch (err) { expect(err.code).toBe("EACCES"); } }); - + it("should create a single folder", async () => { expect.assertions(1); - + const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir1"); - - expect(resolved).toBe(`${MAKEDIR_PATH}/dir1`); + + expect(resolved).toBe(`${MAKEDIR_PATH}${sep}dir1`); }); - + it("should create several folders", async () => { expect.assertions(1); - + const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir2/dir3/dir4"); - - expect(resolved).toBe(`${MAKEDIR_PATH}/dir2/dir3/dir4`); + + expect(resolved).toBe(`${MAKEDIR_PATH}${sep}dir2${sep}dir3${sep}dir4`); }); }); - + describe("makeSymLink", () => { const filePath = `${TESTS_DIR}/file` beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); @@ -230,7 +232,7 @@ describe('FsLocal', () => { try { await localAPI.makeSymlink(filePath, '/non/valid'); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOENT'); } }); @@ -240,7 +242,7 @@ describe('FsLocal', () => { try { await localAPI.makeSymlink(filePath, `${TESTS_DIR}/makedirTest/denied/link`); - } catch(err) { + } catch (err) { expect(err.code).toBe('EACCES'); } }); @@ -250,7 +252,7 @@ describe('FsLocal', () => { try { const res = await localAPI.makeSymlink(filePath, `${TESTS_DIR}/file`); - } catch(err) { + } catch (err) { expect(err.code).toBe('EEXIST'); } }); @@ -258,18 +260,18 @@ describe('FsLocal', () => { describe('rename', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); afterAll(() => mock.restore()); - + it('should throw EEXIST if destination already exists', async () => { expect.assertions(1); try { await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: '14bytes' }, '1024bytes'); - } catch(err) { + } catch (err) { expect(err.code).toBe('EEXIST'); } }); @@ -279,7 +281,7 @@ describe('FsLocal', () => { try { await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: 'nothing_here' }, 'new_file'); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOENT'); } }); @@ -289,7 +291,7 @@ describe('FsLocal', () => { try { await localAPI.rename(`${TESTS_DIR}/deleteTest/folder_denied`, { fullname: 'file2' }, 'new_file'); - } catch(err) { + } catch (err) { expect(err.code).toBe('EACCES'); } }); @@ -311,12 +313,12 @@ describe('FsLocal', () => { describe('isDir', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); afterAll(() => mock.restore()); - + it('should return true if file is a directory', async () => { expect.assertions(1); const res = await localAPI.isDir(TESTS_DIR); @@ -344,7 +346,7 @@ describe('FsLocal', () => { expect.assertions(1); try { await localAPI.isDir(`${TESTS_DIR}/not_here`); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOENT'); } }); @@ -364,7 +366,7 @@ describe('FsLocal', () => { describe('exists', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); @@ -385,7 +387,7 @@ describe('FsLocal', () => { describe('stat', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); mock(TmpDir); }); @@ -449,7 +451,7 @@ describe('FsLocal', () => { try { await localAPI.stat(path); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOENT'); } }); @@ -469,8 +471,8 @@ describe('FsLocal', () => { describe('list', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); - jest.spyOn(localAPI, 'onList').mockImplementation(() => {}); + localAPI = new FsLocal.API("/", () => { }); + jest.spyOn(localAPI, 'onList').mockImplementation(() => { }); mock(TmpDir); }); @@ -484,17 +486,16 @@ describe('FsLocal', () => { expect.assertions(1); try { await localAPI.list('/nothing'); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOENT'); } }); - // TODO: should have called onList(dir) it('should return every files found in path if it exists', async () => { expect.assertions(1); - + const files = await localAPI.list(TESTS_DIR); - + expect(files.length).toBe(6); }); @@ -502,33 +503,42 @@ describe('FsLocal', () => { expect.assertions(1); try { await localAPI.list(`${TESTS_DIR}/deleteTest/file`); - } catch(err) { + } catch (err) { expect(err.code).toBe('ENOTDIR'); } }); - it('should throw EACCES if cannot access folder', async () => { + // mock-fs doesn't check for mode access on Windows + itUnix('should throw EACCES if cannot access folder', async () => { expect.assertions(1); try { await localAPI.list(`${TESTS_DIR}/deleteTest/folder_denied`); - } catch(err) { + } catch (err) { expect(err.code).toBe('EACCES'); } }); - it('should call onList with path when listing a valid directory', async () => { + it('should call onList with path when listing a valid directory and watchDir is true', async () => { expect.assertions(2); - - await localAPI.list(TESTS_DIR); - + + await localAPI.list(TESTS_DIR, true); + expect(localAPI.onList).toHaveBeenCalledTimes(1); expect(localAPI.onList).toHaveBeenCalledWith(TESTS_DIR); }); + + it('should not call onList with path when listing a valid directory and watchDir is false', async () => { + expect.assertions(1); + + await localAPI.list(TESTS_DIR, false); + + expect(localAPI.onList).not.toHaveBeenCalled(); + }); }); describeUnix('isRoot', () => { beforeAll(() => { - localAPI = new FsLocal.API("/", () => {}); + localAPI = new FsLocal.API("/", () => { }); }); it('should return false for empty path', () => { @@ -546,7 +556,7 @@ describe('FsLocal', () => { describeWin('isRoot', () => { beforeAll(() => { - localAPI = new FsLocal.API("C:\\", () => {}); + localAPI = new FsLocal.API("C:\\", () => { }); }); it('should return false for empty path', () => { @@ -566,12 +576,12 @@ describe('FsLocal', () => { }); it('should return false if path === "\\\\foo"', () => { - expect(localAPI.isRoot('\\\\foo')).toBe(true); + expect(localAPI.isRoot('\\\\foo')).toBe(false); }); }); // // describe("delete", function() { // const DELETE_PATH = `${TESTS_DIR}/deleteTest`; - + // beforeAll(() => { // localAPI = new FsLocal.API("/", () => {}); // return prepareTmpTestFiles(); @@ -579,14 +589,14 @@ describe('FsLocal', () => { // it('should resolve if file/folder does not exist', async () => { // expect.assertions(1); - + // const deleted = await localAPI.delete(`${DELETE_PATH}`, [{ // fullname: 'not_here' // }]); - + // expect(deleted).toBe(1); // }); - + // it('should throw EACCESS if file/folder is write protected', async () => { // expect.assertions(1); // process.stdout.write('ro'); @@ -594,17 +604,17 @@ describe('FsLocal', () => { // // const deleted = await localAPI.delete(`${DELETE_PATH}`, [{ // // fullname: 'file_denied' // // }]); - + // // expect(deleted).toBe(2); // expect(1).toBe(2); // }); - + // it("should delete single file and resolve if success", () => {}); - + // it("should delete empty folder and resolve if success", () => {}); - + // it("should delete not empty folder and resolve if success", () => {}); - + // it("should delete not empty folder + files and resolve", () => {}); // }); }) diff --git a/src/state/appState.ts b/src/state/appState.ts index dcce0d30..f9de9c78 100644 --- a/src/state/appState.ts +++ b/src/state/appState.ts @@ -135,15 +135,17 @@ export class AppState { debugger; if ( options.dstPath === cache.path && - options.dstFsName === cache.getFS().name + options.dstFsName === cache.getFS().name && + cache.getFS().options.needsRefresh ) { - // FIX ME: since watcher has been implemented, there's no need to reload cache - // if destination is local fs cache.reload(); } return res; - }); + }) + .catch(err => { + debugger; + }) } /** @@ -167,7 +169,7 @@ export class AppState { if (!srcCache) { srcPath = files[0].dir; const fs = getFS(srcPath); - srcApi = new fs.API(srcPath, () => {}); + srcApi = new fs.API(srcPath, () => { }); } const options = { @@ -232,7 +234,7 @@ export class AppState { } else { // first we need to get a FS for local const fs = getFS(DOWNLOADS_DIR); - const api = new fs.API(DOWNLOADS_DIR, () => {}); + const api = new fs.API(DOWNLOADS_DIR, () => { }); const options = { files, @@ -339,7 +341,7 @@ export class AppState { options.dstFs, options.dstPath ); - debugger; + const batch = new Batch( options.srcFs, options.dstFs, diff --git a/src/state/fileState.ts b/src/state/fileState.ts index f8bb781d..3eb63329 100644 --- a/src/state/fileState.ts +++ b/src/state/fileState.ts @@ -4,7 +4,6 @@ import { Deferred } from '../utils/deferred'; import { i18next } from '../locale/i18n'; import { getLocalizedError } from '../locale/error'; import { shell, ipcRenderer } from 'electron'; -import * as process from 'process'; import { AppState } from "./appState"; import { TSORT_METHOD_NAME, TSORT_ORDER } from "../services/FsSort"; @@ -438,7 +437,7 @@ export class FileState { @action @needsConnection async list(path: string): Promise { - return this.api.list(path) + return this.api.list(path, true) .catch(this.handleError) } @@ -540,7 +539,7 @@ export class FileState { } } - shellOpenFile(path: string):boolean { + shellOpenFile(path: string): boolean { console.log('need to open file', path); return shell.openItem(path); } diff --git a/src/transfers/batch.ts b/src/transfers/batch.ts index d200844d..3f2aa7da 100644 --- a/src/transfers/batch.ts +++ b/src/transfers/batch.ts @@ -5,6 +5,7 @@ import { Deferred } from "../utils/deferred"; import { getLocalizedError } from "../locale/error"; import { Readable } from "stream"; import { getSelectionRange } from "../utils/fileUtils"; +import { isWin } from "../utils/platform"; const MAX_TRANSFERS = 2; const MAX_ERRORS = 5; @@ -79,7 +80,7 @@ export class Batch { @action start(): Promise { - console.log('starting batch'); + console.log('Starting batch'); if (this.status === 'queued') { this.slotsAvailable = MAX_TRANSFERS; this.status = 'started'; @@ -102,7 +103,8 @@ export class Batch { @action updatePendingTransfers(subDir: string, newFilename: string, cancel = false) { // TODO: escape '(' & ')' in subDir if needed - const regExp = new RegExp('^(' + subDir + ')'); + const escapedSubDir = isWin ? subDir.replace(/\\/g, '\\\\') : subDir; + const regExp = new RegExp(`^(${escapedSubDir})`); const files = this.files.filter((file) => file.subDirectory.match(regExp) !== null); // destination directory for these files could not be created: we cancel these transfers @@ -190,9 +192,9 @@ export class Batch { let stream = null; try { - if (transfer.file.isSym) { - debugger; - } + // if (transfer.file.isSym) { + // debugger; + // } newFilename = await this.renameOrCreateDir(transfer, fullDstPath); } catch (err) { console.log('error creating directory', err); @@ -207,21 +209,19 @@ export class Batch { if (isSym) { const linkPath = dstFs.join(fullDstPath, newFilename) try { - debugger; await srcFs.makeSymlink(transfer.file.target, linkPath, this.id) transfer.status = 'done'; - } catch(err) { + } catch (err) { this.onTransferError(transfer, err); - debugger; } } else { try { - if (transfer.file.isSym) { - debugger; - } + // if (transfer.file.isSym) { + // debugger; + // } // console.log('getting stream', srcPath, wantedName); stream = await srcFs.getStream(srcPath, wantedName, this.id); - + this.streams.push(stream); // console.log('sending to stream', dstFs.join(fullDstPath, newFilename)); // we have to listen for errors that may appear during the transfer: socket closed, timeout,... @@ -233,11 +233,12 @@ export class Batch { // // destroy stream so that put stream resolves ? // stream.destroy(); // }); - + await dstFs.putStream(stream, dstFs.join(fullDstPath, newFilename), (bytesRead: number) => { this.onData(transfer, bytesRead); }, this.id); - + + console.log('endPutStream', fullDstPath); this.removeStream(stream); transfer.status = 'done'; } catch (err) { @@ -253,13 +254,14 @@ export class Batch { //} this.onTransferError(transfer, err); if (this.errors > MAX_ERRORS) { - console.warn('maximum errors occurred: cancelling upcoming file transfers'); + console.warn('Maximum errors occurred: cancelling upcoming file transfers'); this.status = 'error'; this.cancelFiles(); } } } } else { + // console.log('isDir', fullDstPath); transfer.status = 'done'; // make transfers with this directory ready this.updatePendingTransfers(srcFs.join(transfer.subDirectory, wantedName), newFilename, (transfer as FileTransfer).status !== 'done'); @@ -275,23 +277,11 @@ export class Batch { if (this.status !== 'error' && this.transfersDone < this.files.length) { this.queueNextTransfers(); } else { - for (let transfer of this.files) { - console.log(transfer.status, transfer); - } - // console.log('no more transfers !!'); if (this.numErrors === this.files.length) { - for (let transfer of this.files) { - console.log(transfer.status, transfer.file.fullname, transfer); - } - this.transferDef.reject({ code: '' }); } else { - for (let transfer of this.files) { - console.log(transfer.status, transfer.file.fullname, transfer); - } - this.transferDef.resolve(); } } @@ -480,6 +470,7 @@ export class Batch { async setFileList(files: File[]) { return this.getFileList(files).then((transfers) => { console.log('got files', transfers); + // transfers.forEach(t => console.log(`[${t.status}] ${t.file.dir}/${t.file.fullname}:${t.file.isDir}, ${t.subDirectory} (${t.newSub})`)) this.files.replace(transfers); }).catch((err) => { return Promise.reject(err); diff --git a/src/utils/test/helpers.ts b/src/utils/test/helpers.ts index 776b0ad6..84c70054 100644 --- a/src/utils/test/helpers.ts +++ b/src/utils/test/helpers.ts @@ -1,14 +1,14 @@ import { platform } from 'process'; import { homedir, tmpdir } from 'os'; -import { mkdirSync, writeFileSync, existsSync } from 'fs'; -import { execSync } from 'child_process'; +import { sep } from 'path'; // expects describe to be globally defined: should only be used from test environment declare var describe: any; +declare var it: any; // assume Unix in that case: may need some tweaks for some exotic platform const isUnix = platform !== 'win32'; -const TEST_FILES_DIR = tmpdir() + '/react-explorer-tests'; +const TEST_FILES_DIR = tmpdir() + sep + 'react-explorer-tests'; // call this to perform unix specific tests export const describeUnix = (...args: any) => { @@ -30,6 +30,16 @@ export const describeWin = (...args: any) => { } }; +// call this to perform unix specific tests +export const itUnix = (...args: any) => { + args[0] += ' (Unix)'; + if (isUnix) { + it(...args); + } else { + it.skip(...args); + } +}; + export const getPath = (id: string) => { switch (id) { case 'home': @@ -45,65 +55,3 @@ export const getPath = (id: string) => { return tmpdir(); } } - -function createEmptyFile(path: string, bytes = 0, mode = 0o777) { - // console.log('creating file', path, bytes); - writeFileSync(path, Buffer.alloc(bytes), { - mode - }); -} - -// create some test files/directories for testing local fs functions -// @todo handle windows style separators -export const prepareTmpTestFiles = () => { - return new Promise((resolve, reject) => { - // console.log('tmpdir', TEST_FILES_DIR); - const tmpSizeDir = `${TEST_FILES_DIR}/sizeTest`; - const tmpMakedirDir = `${TEST_FILES_DIR}/makedirTest` - const tmpDeleteDir = `${TEST_FILES_DIR}/deleteTest` - - // delete tmpdir if it exits - // console.log('deleting dir', TEST_FILES_DIR); - // first make this folder readable so that it can be removed - if (existsSync(`${tmpMakedirDir}/denied`)) { - execSync(`chmod 777 ${tmpMakedirDir}/denied`); - } - - if (existsSync(`${tmpDeleteDir}/folder_denied`)) { - execSync(`chmod 777 ${tmpDeleteDir}/folder_denied`); - } - - if (existsSync(`${tmpDeleteDir}/file_denied`)) { - execSync(`chmod 777 ${tmpDeleteDir}/file_denied`); - } - - if (existsSync(TEST_FILES_DIR)) { - execSync(`rm -rf ${TEST_FILES_DIR}`); - } - - // console.log('creating dir', TEST_FILES_DIR); - // recreate directory - mkdirSync(TEST_FILES_DIR); - - // size test - mkdirSync(tmpSizeDir); - createEmptyFile(`${tmpSizeDir}/14bytes`, 14); - createEmptyFile(`${tmpSizeDir}/1024bytes`, 1024); - - // makedir test - mkdirSync(tmpMakedirDir); - mkdirSync(`${tmpMakedirDir}/denied`, 0o000); - - // delete test - mkdirSync(tmpDeleteDir); - mkdirSync(`${tmpDeleteDir}/folder_denied`, 0o000); - - mkdirSync(`${tmpDeleteDir}/single_folder`); - mkdirSync(`${tmpDeleteDir}/multiple_folder`); - - createEmptyFile(`${tmpDeleteDir}/file`, 1024); - createEmptyFile(`${tmpDeleteDir}/file_denied`, 1024, 0o000); - - resolve(); - }); -} \ No newline at end of file