filefive 1.0.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,13 +4,11 @@
4
4
 
5
5
 
6
6
  # FileFive: SFTP/FTP client and dual-panel file manager for macOS and Linux
7
- FileFive is a free open-source SFTP/FTP client and file manager with intuitive and modern dual-panel interface.
7
+ FileFive is a free open-source SFTP/FTP client and file manager with intuitive and modern dual-panel interface, available on Mac, and Linux.
8
8
 
9
9
  It is installed as a Node.js package and uses the web browser as GUI.
10
10
 
11
- > [!NOTE]
12
- > This project is being actively developed. Feel free to create any [issues](https://github.com/miroshnikov/filefive/issues), [feature requests](https://github.com/miroshnikov/filefive/issues) or [enhancements](https://github.com/miroshnikov/filefive/discussions) as you encounter them.
13
-
11
+ FileFive has a unique set of features and may be an alternative to FileZilla, Cyberduck, Transmit, ForkLift and Commander One.
14
12
 
15
13
  <p align="center">
16
14
  <img src="https://github.com/miroshnikov/filefive/blob/main/screenshot.png" alt="FileFive" />
@@ -33,20 +31,24 @@ Options:
33
31
  -V, --version output the version number
34
32
  -p, --port <number> port number (default: "3113")
35
33
  --log prints the log information
36
- -h, --help display help for command
34
+ -h, --help display help
37
35
  ```
38
36
 
39
37
  ## Features
38
+ - Cross-platform, runs on Mac OS, Linux and any *nix with Node.js
40
39
  - Supports SSH File Transfer Protocol (SFTP) and FTP
41
- - Cross-platform, runs on Mac OS, Linux and any *nix
42
- - Minimal and intuitive UI, mimicing the VSCode Explorer view
43
- - Search/filter files using JavaScript Regular Expressions
40
+ - Minimalistic and intuitive UI, mimicing the look and feel of VSCode Explorer view
41
+ - Search/filter files using wildcards and JavaScript Regular Expressions
44
42
  - Synchronized browsing
43
+ - Remote file editing
45
44
  - Connections/servers are plain files stored on your filesystem in the `~/.f5/connections` folder
46
- - You can use Git or any VCS to store connections and settings
47
- - Drag & drop support
48
- - Open files and folders in Visual Studio Code
49
- - Uses browser tabs to browse more than one server or transfer files simultaneously
45
+ - Easy to backup connections and settings in `~/.f5` folder, e.g. by putting them into a Git repo
46
+ - Drag & drop, copy & paste files support
47
+ - Use browser tabs to browse more than one server or transfer files simultaneously
48
+ - Utilize the built-in browser password manager to store passwords
49
+ - Open files and folders in default app or Visual Studio Code
50
+ - Theming: System preference, Light, Dark; a different color theme per connection
51
+ - Search On Type
50
52
 
51
53
  ## Feedbacks
52
54
  To support its development, [star FileFive on GitHub](https://github.com/miroshnikov/filefive/stargazers)!
package/dist/App.js CHANGED
@@ -34,16 +34,16 @@ class App {
34
34
  getapp: () => commands_1.commands.getSettings(settingsPath),
35
35
  saveapp: ({ settings }) => commands_1.commands.saveSettings(settingsPath, settings),
36
36
  connect: ({ file }) => commands_1.commands.connect(file, (id, { message }) => this.onError({ type: types_1.FailureType.RemoteError, id, message })),
37
- login: ({ id, password, remember }) => Password_1.default.set(id, password, remember),
37
+ login: ({ id, password, remember }) => Password_1.default.set(id, password, remember, false),
38
38
  disconnect: ({ id, sid }) => commands_1.commands.disconnect(id, sid),
39
39
  watch: ({ dir }) => commands_1.commands.watch(dir, this.localWatcher, this.remoteWatcher, this.fileWatcher),
40
40
  unwatch: ({ dir }) => commands_1.commands.unwatch(dir, this.localWatcher, this.remoteWatcher, this.fileWatcher),
41
41
  refresh: ({ dir }) => this.remoteWatcher.refresh(dir),
42
42
  copy: ({ src, dest, move, filter, sid }) => commands_1.commands.copy(src, dest, move, filter, sid),
43
43
  duplicate: ({ src, filter }) => commands_1.commands.duplicate(src, filter),
44
- remove: ({ files, force }) => commands_1.commands.remove(files, force, connPath),
45
- clear: ({ file, force }) => commands_1.commands.clear(file, force),
46
- open: ({ file }) => opener(file),
44
+ remove: ({ files }) => commands_1.commands.remove(files, connPath),
45
+ clear: ({ file }) => commands_1.commands.clear(file),
46
+ open: ({ file, app }) => commands_1.commands.open(file, app, opener),
47
47
  mkdir: ({ name, parent }) => commands_1.commands.mkdir(name, parent),
48
48
  read: ({ file }) => commands_1.commands.read(file),
49
49
  write: ({ path, content }) => commands_1.commands.write(path, content),
@@ -64,6 +64,20 @@ class App {
64
64
  this.fileWatcher = new FileWatcher_1.default(sendFileStat);
65
65
  const emitQueue = emitter('queue');
66
66
  this.onQueueUpdate = (id, event) => emitQueue({ id, event });
67
+ const notifyNewVer = async () => {
68
+ const versions = await commands_1.commands.checkVer();
69
+ if (versions) {
70
+ this.onError({
71
+ type: types_1.FailureType.Warning,
72
+ message: `
73
+ <p>A new version of the FileFive is available!</p>
74
+ <p>Current version: <em>${versions[0]}</em> → New version: <em>${versions[1]}</em></p>
75
+ `
76
+ });
77
+ }
78
+ };
79
+ setTimeout(notifyNewVer, 20000);
80
+ setInterval(notifyNewVer, 86400000);
67
81
  }
68
82
  }
69
83
  exports.default = App;
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const ReferenceCountMap_1 = __importDefault(require("./utils/ReferenceCountMap"));
40
40
  const types_1 = require("./types");
41
41
  const URI_1 = require("./utils/URI");
42
- const Password_1 = __importDefault(require("./Password"));
43
42
  const uniqid_1 = __importDefault(require("./utils/uniqid"));
44
43
  const log_1 = __importStar(require("./log"));
45
44
  const Local_1 = __importDefault(require("./fs/Local"));
@@ -50,12 +49,13 @@ class default_1 {
50
49
  static initialize() {
51
50
  this.shared.set(types_1.LocalFileSystemID, new Local_1.default);
52
51
  }
53
- static async open(scheme, user, host, port) {
52
+ static async open(scheme, user, host, port, password) {
54
53
  const id = (0, URI_1.connectionID)(scheme, user, host, port);
55
54
  const attrs = (scheme == 'sftp') ? SFtp_1.ATTRIBUTES : Ftp_1.ATTRIBUTES;
56
55
  if (this.shared.inc(id)) {
57
56
  return attrs;
58
57
  }
58
+ this.credentials.set(id, password);
59
59
  const conn = await this.create(id, scheme, user, host, port);
60
60
  await conn.open();
61
61
  this.shared.set(id, conn);
@@ -161,9 +161,9 @@ class default_1 {
161
161
  }
162
162
  static async create(id, scheme, user, host, port, onClose = () => { }) {
163
163
  if (options_1.default.log) {
164
- return new log_1.LogFS(id, await this.createFS(scheme, user, host, port, await Password_1.default.get(id), onClose));
164
+ return new log_1.LogFS(id, await this.createFS(scheme, user, host, port, this.credentials.get(id), onClose));
165
165
  }
166
- return this.createFS(scheme, user, host, port, await Password_1.default.get(id), onClose);
166
+ return this.createFS(scheme, user, host, port, this.credentials.get(id), onClose);
167
167
  }
168
168
  static async createFS(scheme, user, host, port, password, onClose) {
169
169
  switch (scheme) {
@@ -185,9 +185,9 @@ class default_1 {
185
185
  default_1.numOfStartups = 0;
186
186
  default_1.maxStartups = 7; // for SFTP see MaxStartups in /etc/ssh/sshd_config
187
187
  default_1.shared = new ReferenceCountMap_1.default;
188
- // private static pools: Record<string, Record<string, {fs: FileSystem, idle: false|ReturnType<typeof setTimeout>}>> = {}
189
188
  default_1.pools = new Map();
190
189
  default_1.queue = [];
191
190
  default_1.pending = {};
192
191
  default_1.limits = new Map();
192
+ default_1.credentials = new Map();
193
193
  exports.default = default_1;
package/dist/Password.js CHANGED
@@ -8,19 +8,16 @@ class Passwords {
8
8
  this.resolve = onMiss;
9
9
  this.store = new Map(JSON.parse((await (0, promises_1.readFile)(this.saveFile)).toString()).map(([id, password]) => [id, [password, true]]));
10
10
  }
11
- static set(id, password, remember = false) {
11
+ static set(id, password, remember, save) {
12
12
  if (password === false) {
13
13
  this.pending.get(id)?.[1]();
14
14
  }
15
15
  else {
16
16
  this.pending.get(id)?.[0](password);
17
- remember && this.store.set(id, [password, false]);
17
+ remember && this.store.set(id, [password, save]);
18
+ save && this.dump();
18
19
  }
19
20
  }
20
- static save(id, password) {
21
- this.store.set(id, [password, true]);
22
- this.dump();
23
- }
24
21
  static async get(id, skipMissing = false) {
25
22
  if (this.store.has(id)) {
26
23
  return this.store.get(id)[0];
@@ -32,11 +29,11 @@ class Passwords {
32
29
  this.resolve(id);
33
30
  return p;
34
31
  }
35
- static delete(id, saved) {
32
+ static delete(id, save) {
36
33
  const found = this.store.get(id);
37
- if (found && found[1] == saved) {
34
+ if (found && (save || found[1] === save)) {
38
35
  this.store.delete(id);
39
- found[1] == true && this.dump();
36
+ found[1] === true && this.dump();
40
37
  }
41
38
  this.pending.delete(id);
42
39
  }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.open = open;
7
+ const types_1 = require("./types");
8
+ const URI_1 = require("./utils/URI");
9
+ const commands_1 = require("./commands");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ const promises_1 = require("node:fs/promises");
13
+ const FileWatcher_1 = __importDefault(require("./FileWatcher"));
14
+ const Connection_1 = __importDefault(require("./Connection"));
15
+ const App_1 = __importDefault(require("./App"));
16
+ const files = new Map();
17
+ const watcher = new FileWatcher_1.default(path => send(path));
18
+ async function open(file, onLoad) {
19
+ const { path } = (0, URI_1.parseURI)(file);
20
+ try {
21
+ const tmpDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'f5-'));
22
+ const tmpName = (0, node_path_1.join)(tmpDir, (0, node_path_1.basename)(path));
23
+ return commands_1.commands.copy([file], (0, URI_1.createURI)(types_1.LocalFileSystemID, tmpDir), false, null, null, () => {
24
+ files.set(tmpName, { file, deletion: resetDeletion(tmpName, null), sending: false, changed: false });
25
+ watcher.watch(tmpName);
26
+ onLoad(tmpName);
27
+ });
28
+ }
29
+ catch (err) {
30
+ console.error(err);
31
+ }
32
+ }
33
+ async function send(file) {
34
+ const watched = files.get(file);
35
+ if (!watched) {
36
+ return;
37
+ }
38
+ if (watched.sending) {
39
+ watched.changed = true;
40
+ return;
41
+ }
42
+ watched.sending = true;
43
+ watched.deletion = resetDeletion(file, watched.deletion);
44
+ const { id, path } = (0, URI_1.parseURI)(watched.file);
45
+ const [conn, close] = await Connection_1.default.transmit(id);
46
+ await conn.put(file, path);
47
+ close();
48
+ App_1.default.remoteWatcher.refresh((0, URI_1.createURI)(id, (0, node_path_1.dirname)(path)));
49
+ watched.sending = false;
50
+ if (watched.changed) {
51
+ watched.changed = false;
52
+ send(file);
53
+ }
54
+ }
55
+ function resetDeletion(file, current) {
56
+ clearTimeout(current);
57
+ return setTimeout(() => {
58
+ files.delete(file);
59
+ (0, promises_1.rm)((0, node_path_1.dirname)(file), { force: true, recursive: true });
60
+ }, 1000 * 60 * 60);
61
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = default_1;
7
+ const package_json_1 = __importDefault(require("../../package.json"));
8
+ async function default_1() {
9
+ // https://github.com/npm/registry
10
+ const server = await fetch('https://registry.npmjs.com/filefive/latest').then(resp => resp.json());
11
+ if (typeof server === 'object' && 'version' in server) {
12
+ return server.version == package_json_1.default.version ? null : [package_json_1.default.version, server.version];
13
+ }
14
+ return null;
15
+ }
@@ -9,11 +9,7 @@ const types_1 = require("../types");
9
9
  const URI_1 = require("../utils/URI");
10
10
  const Connection_1 = __importDefault(require("../Connection"));
11
11
  const App_1 = __importDefault(require("../App"));
12
- async function default_1(file, force) {
13
- if (!force) {
14
- App_1.default.onError({ type: types_1.FailureType.ConfirmClear, file });
15
- return;
16
- }
12
+ async function default_1(file) {
17
13
  const { id, path } = (0, URI_1.parseURI)(file);
18
14
  await Connection_1.default.get(id).write(path, '');
19
15
  if (id != types_1.LocalFileSystemID) {
@@ -55,14 +55,18 @@ async function default_1(file, onError) {
55
55
  throw new Error(`Invalid connection file ${file}`);
56
56
  }
57
57
  const id = (0, URI_1.connectionID)(config.scheme, config.user, config.host, config.port);
58
+ let password = '';
58
59
  try {
59
- await Password_1.default.get(id);
60
+ password = await Password_1.default.get(id);
60
61
  }
61
62
  catch (e) {
62
63
  return false;
63
64
  }
65
+ if (!password) {
66
+ return false;
67
+ }
64
68
  try {
65
- const attributes = await Connection_1.default.open(config.scheme, config.user, config.host, config.port);
69
+ const attributes = await Connection_1.default.open(config.scheme, config.user, config.host, config.port, password);
66
70
  const pwd = await Connection_1.default.get(id).pwd();
67
71
  const settings = {
68
72
  name: (0, path_1.parse)(file).name,
@@ -59,7 +59,7 @@ function default_1(src, dest, move, filter, sid, onComplete = () => { }) {
59
59
  new Upload_1.default(connection, from, toPath, filter, state => App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Update, state }), onConflict.bind(queue), error => App_1.default.onError(error), onFinish, App_1.default.remoteWatcher);
60
60
  }
61
61
  Queue_1.queues.set(id, queue);
62
- App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Create, queueType, connection });
63
62
  queue.create();
63
+ setTimeout(() => App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Create, queueType, connection }), 100);
64
64
  return id;
65
65
  }
@@ -13,6 +13,7 @@ const remove_1 = __importDefault(require("./remove"));
13
13
  const clear_1 = __importDefault(require("./clear"));
14
14
  const mkdir_1 = __importDefault(require("./mkdir"));
15
15
  const read_1 = __importDefault(require("./read"));
16
+ const open_1 = __importDefault(require("./open"));
16
17
  const write_1 = __importDefault(require("./write"));
17
18
  const settings_1 = __importDefault(require("./settings"));
18
19
  const rename_1 = __importDefault(require("./rename"));
@@ -21,6 +22,7 @@ const saveConnection_1 = __importDefault(require("./saveConnection"));
21
22
  const saveSettings_1 = __importDefault(require("./saveSettings"));
22
23
  const duplicate_1 = __importDefault(require("./duplicate"));
23
24
  const resolve_1 = __importDefault(require("./resolve"));
25
+ const checkVer_1 = __importDefault(require("./checkVer"));
24
26
  exports.commands = {
25
27
  connect: connect_1.default,
26
28
  disconnect: disconnect_1.default,
@@ -33,10 +35,12 @@ exports.commands = {
33
35
  clear: clear_1.default,
34
36
  mkdir: mkdir_1.default,
35
37
  read: read_1.default,
38
+ open: open_1.default,
36
39
  write: write_1.default,
37
40
  rename: rename_1.default,
38
41
  getConnection: getConnection_1.default,
39
42
  saveConnection: saveConnection_1.default,
40
43
  saveSettings: saveSettings_1.default,
41
- resolve: resolve_1.default
44
+ resolve: resolve_1.default,
45
+ checkVer: checkVer_1.default
42
46
  };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const URI_1 = require("../utils/URI");
5
+ const RemoteFiles_1 = require("../RemoteFiles");
6
+ async function default_1(file, app, opener) {
7
+ if ((0, URI_1.isLocal)(file)) {
8
+ const { id, path } = (0, URI_1.parseURI)(file);
9
+ if (app == 'code') {
10
+ // open files through protocol links
11
+ // vscode://file/<path>
12
+ // vscode-insiders://file/<path>
13
+ opener(`vscode://file/${path}`);
14
+ }
15
+ else {
16
+ opener(path);
17
+ }
18
+ return '';
19
+ }
20
+ else {
21
+ return (0, RemoteFiles_1.open)(file, path => opener(app == 'code' ? `vscode://file/${path}` : path));
22
+ }
23
+ }
@@ -14,14 +14,10 @@ const Remove_1 = __importDefault(require("../queues/Remove"));
14
14
  const trash = import("trash");
15
15
  const App_1 = __importDefault(require("../App"));
16
16
  const ramda_1 = require("ramda");
17
- async function default_1(files, force, connPath, immediately = false) {
17
+ async function default_1(files, connPath, immediately = false) {
18
18
  if (!files.length) {
19
19
  return;
20
20
  }
21
- if (!force) {
22
- App_1.default.onError({ type: types_1.FailureType.ConfirmDeletion, files });
23
- return;
24
- }
25
21
  if ((0, URI_1.isLocal)(files[0])) {
26
22
  const paths = files.map(p => (0, URI_1.parseURI)(p)['path']);
27
23
  if (immediately) {
@@ -35,6 +31,7 @@ async function default_1(files, force, connPath, immediately = false) {
35
31
  // TODO delete from credentials
36
32
  }
37
33
  });
34
+ return null;
38
35
  }
39
36
  else {
40
37
  const connId = (0, URI_1.parseURI)(files[0])['id'];
@@ -50,7 +47,8 @@ async function default_1(files, force, connPath, immediately = false) {
50
47
  App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Complete });
51
48
  }, App_1.default.remoteWatcher);
52
49
  Queue_1.queues.set(id, queue);
53
- App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Create, queueType: types_1.QueueType.Remove, connection: connId });
54
50
  queue.create();
51
+ setTimeout(() => App_1.default.onQueueUpdate(id, { type: types_1.QueueEventType.Create, queueType: types_1.QueueType.Remove, connection: connId }), 100);
52
+ return id;
55
53
  }
56
54
  }
@@ -26,9 +26,11 @@ async function default_1(path, settings) {
26
26
  }
27
27
  let config = content ? JSON.parse(content) : {};
28
28
  if ('scheme' in settings) {
29
- config = { ...config, ...(0, ramda_1.omit)(['password'], settings) };
29
+ const id = (0, URI_1.connectionID)(settings.scheme, settings.user, settings.host, settings.port);
30
+ config = { ...config, ...(0, ramda_1.omit)(['password', 'savePassword'], settings) };
31
+ Password_1.default.delete(id, true);
30
32
  if (settings.password.length) {
31
- Password_1.default.save((0, URI_1.connectionID)(settings.scheme, settings.user, settings.host, settings.port), settings.password);
33
+ Password_1.default.set(id, settings.password, true, settings.savePassword);
32
34
  }
33
35
  }
34
36
  else {
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ const handle = async (name, handler) => {
46
46
  }
47
47
  catch (e) {
48
48
  res.status(400);
49
- process.env.NODE_ENV == 'development' && console.error(e);
49
+ process.env.NODE_ENV == 'development' && console.error('API error: ', e);
50
50
  res.json({ message: (typeof e == 'object' && 'message' in e) ? e.message : String(e) });
51
51
  }
52
52
  });
@@ -19,6 +19,10 @@
19
19
  "key": "meta+a",
20
20
  "command": "select-all"
21
21
  },
22
+ {
23
+ "key": "meta+shift+a",
24
+ "command": "select-all-files"
25
+ },
22
26
  {
23
27
  "key": "ctrl+r",
24
28
  "command": "refresh"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.5",
2
+ "version": "0.0.0",
3
3
  "api_version": 1,
4
4
  "layout": {
5
5
  "logo": "yandex-browser-50x50.png",