diff --git a/README.md b/README.md index cb46171..ae0a56c 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,19 @@ await api.directory.list('/', async (remote_path, page_count, get_page) => { } }); +// Asynchronous directory list +const snap = await api.directory.snapshot('/'); +try { + for (let i = 0; i < snap.page_count; i++) { + const items = await snap.get_page(i); // Always 'await' + console.log(items); + } +} catch (err) { + console.log(err); +} finally { + await snap.release(); +} + // Create new directory await api.directory.create('/test'); diff --git a/src/__tests__/repertory.test.js b/src/__tests__/repertory.test.js index 58e5dc5..728ca23 100644 --- a/src/__tests__/repertory.test.js +++ b/src/__tests__/repertory.test.js @@ -91,12 +91,12 @@ test('can create and remove a directory using api', async () => { await conn.disconnect(); }); -test('can get directory list using api', async () => { +test('can get directory list and snapshot using api', async () => { const conn = - await repertory.create_pool(2, TEST_HOST, TEST_PORT, TEST_PASSWORD); + await repertory.create_pool(2, TEST_HOST, TEST_PORT, TEST_PASSWORD); const api = repertory.create_api(conn); - await api.directory.list('/', async (remote_path, page_count, get_page) => { - console.log(remote_path, page_count, get_page); + + const test_results = async (remote_path, page_count, get_page) => { expect(remote_path).toEqual('/'); expect(page_count).toBeGreaterThanOrEqual(1); expect(get_page).toBeInstanceOf(Function); @@ -110,8 +110,23 @@ test('can get directory list using api', async () => { expect(items[1].directory).toBeTruthy(); expect(items[1].path).toEqual('..'); } + }; + + await api.directory.list('/', async (remote_path, page_count, get_page) => { + console.log(remote_path, page_count, get_page); + await test_results(remote_path, page_count, get_page); }); + const snap = await api.directory.snapshot('/'); + try { + console.log(snap.remote_path, snap.page_count, snap.get_page); + await test_results(snap.remote_path, snap.page_count, snap.get_page); + } catch (err) { + console.log(err); + } finally { + await snap.release(); + } + await conn.disconnect(); }); diff --git a/src/index.js b/src/index.js index 7cc2e4c..7160981 100644 --- a/src/index.js +++ b/src/index.js @@ -12,10 +12,13 @@ export const connect = async (host_or_ip, port, password) => { export const create_api = conn => { return { directory : { - list : async (remote_path, page_reader_cb) => - ops.list_directory(conn, remote_path, page_reader_cb), - create : async remote_path => ops.create_directory(conn, remote_path), - remove : async remote_path => ops.remove_directory(conn, remote_path), + create: async remote_path => ops.create_directory(conn, remote_path), + list: async (remote_path, page_reader_cb) => + ops.list_directory(conn, remote_path, page_reader_cb), + remove: async remote_path => ops.remove_directory(conn, remote_path), + snapshot: async remote_path => { + return ops.snapshot_directory(conn, remote_path); + }, }, file : { create_or_open : async remote_path => new file( diff --git a/src/ops/index.js b/src/ops/index.js index ea85cac..2b41d41 100644 --- a/src/ops/index.js +++ b/src/ops/index.js @@ -4,23 +4,86 @@ import {Uint64BE} from 'int64-buffer'; import file from '../io/file'; import packet from '../networking/packet'; -export const close_file = - async (conn, remote_path, handle, optional_thread_id) => { +const _snapshot_directory = async (conn, remote_path) => { try { const request = new packet(); request.encode_utf8(remote_path); - request.encode_ui64(handle); const response = - await conn.send('::RemoteFUSERelease', request, optional_thread_id); + await conn.send('::RemoteJSONCreateDirectorySnapshot', request); response.decode_ui32(); // Service flags - return response.decode_i32(); + const result = response.decode_i32(); + if (result === 0) { + const data = JSON.parse(response.decode_utf8()); + + let released = false; + const release = async () => { + if (!released) { + released = true; + const request = new packet(); + request.encode_ui64(data.handle); + await conn.send('::RemoteJSONReleaseDirectorySnapshot', request); + } + }; + + try { + const get_page = async page => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(data.handle); + request.encode_ui32(page); + + const response = + await conn.send('::RemoteJSONReadDirectorySnapshot', request); + response.decode_ui32(); // Service flags + + const result = response.decode_i32(); + if (result === 0 || result === -120) { + const data = JSON.parse(response.decode_utf8()); + return data.directory_list; + } + } catch (err) { + await release(); + return Promise.reject(new Error(`'get_page' failed: ${err}`)); + } + return []; + }; + + return { + get_page, + page_count: data.page_count, + release, + remote_path, + }; + } catch (err) { + await release(); + return Promise.reject(new Error(`'snapshot_directory' failed: ${err}`)); + } + } } catch (err) { - return Promise.reject(new Error(`'close_file' failed: ${err}`)); + return Promise.reject(new Error(`'snapshot_directory' failed: ${err}`)); } }; +export const close_file = + async (conn, remote_path, handle, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(handle); + + const response = + await conn.send('::RemoteFUSERelease', request, optional_thread_id); + response.decode_ui32(); // Service flags + + return response.decode_i32(); + } catch (err) { + return Promise.reject(new Error(`'close_file' failed: ${err}`)); + } + }; + export const create_directory = async (conn, remote_path) => { try { const request = new packet(); @@ -37,27 +100,27 @@ export const create_directory = async (conn, remote_path) => { }; export const create_or_open_file = - async (conn, remote_path, optional_thread_id) => { - try { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui16((7 << 6) | (5 << 3)); - request.encode_ui32(2 | 4); // Read-Write, Create + async (conn, remote_path, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui16((7 << 6) | (5 << 3)); + request.encode_ui32(2 | 4); // Read-Write, Create - const response = + const response = await conn.send('::RemoteFUSECreate', request, optional_thread_id); - response.decode_ui32(); // Service flags + response.decode_ui32(); // Service flags - const result = response.decode_i32(); - if (result === 0) { - return response.decode_ui64(); + const result = response.decode_i32(); + if (result === 0) { + return response.decode_ui64(); + } + + return Promise.reject(new Error(`'create_or_open_file' error: ${result}`)); + } catch (err) { + return Promise.reject(new Error(`'create_or_open_file' failed: ${err}`)); } - - return Promise.reject(new Error(`'create_or_open_file' error: ${result}`)); - } catch (err) { - return Promise.reject(new Error(`'create_or_open_file' failed: ${err}`)); - } -}; + }; export const delete_file = async (conn, remote_path) => { try { @@ -74,96 +137,96 @@ export const delete_file = async (conn, remote_path) => { }; export const download_file = - async (conn, remote_path, local_path, progress_cb, overwrite, resume) => { - try { - const src = new file(conn, await open_file(conn, remote_path), remote_path); - const cleanup = async fd => { - try { - await src.close(); - } catch (err) { - console.log(err); - } - try { - if (fd !== undefined) { - fs.closeSync(fd); - } - } catch (err) { - console.log(err); - } - }; - + async (conn, remote_path, local_path, progress_cb, overwrite, resume) => { try { - const src_size = await src.get_size(); - let dst_fd; + const src = new file(conn, await open_file(conn, remote_path), remote_path); + const cleanup = async fd => { + try { + await src.close(); + } catch (err) { + console.log(err); + } + try { + if (fd !== undefined) { + fs.closeSync(fd); + } + } catch (err) { + console.log(err); + } + }; try { - let offset = 0; - if (overwrite) { - dst_fd = fs.openSync(local_path, 'w+'); - } else if (resume) { - dst_fd = fs.openSync(local_path, 'r+'); + const src_size = await src.get_size(); + let dst_fd; - const dst_size = fs.fstatSync(dst_fd).size; - if (dst_size === src_size) { - await cleanup(dst_fd); - return true; - } + try { + let offset = 0; + if (overwrite) { + dst_fd = fs.openSync(local_path, 'w+'); + } else if (resume) { + dst_fd = fs.openSync(local_path, 'r+'); - if (dst_size > src_size) { - await cleanup(dst_fd); - return Promise.reject(new Error( + const dst_size = fs.fstatSync(dst_fd).size; + if (dst_size === src_size) { + await cleanup(dst_fd); + return true; + } + + if (dst_size > src_size) { + await cleanup(dst_fd); + return Promise.reject(new Error( `'download_file' failed: destination is larger than source`)); - } + } - offset = dst_size; - } else { - if (fs.existsSync(local_path)) { - await cleanup(dst_fd); - return Promise.reject( + offset = dst_size; + } else { + if (fs.existsSync(local_path)) { + await cleanup(dst_fd); + return Promise.reject( new Error(`'download_file' failed: file exists`)); + } + + dst_fd = fs.openSync(local_path, 'wx+'); } - dst_fd = fs.openSync(local_path, 'wx+'); - } - - let remain = src_size - offset; - while (remain > 0) { - const to_write = remain >= 65536 ? 65536 : remain; - const buffer = await src.read(offset, to_write); - const written = fs.writeSync(dst_fd, buffer, 0, to_write, offset); - if (written > 0) { - remain -= written; - offset += written; - if (progress_cb) { - progress_cb(local_path, remote_path, - ((src_size - remain) / src_size) * 100.0, false); + let remain = src_size - offset; + while (remain > 0) { + const to_write = remain >= 65536 ? 65536 : remain; + const buffer = await src.read(offset, to_write); + const written = fs.writeSync(dst_fd, buffer, 0, to_write, offset); + if (written > 0) { + remain -= written; + offset += written; + if (progress_cb) { + progress_cb(local_path, remote_path, + ((src_size - remain) / src_size) * 100.0, false); + } } } - } - if (progress_cb) { - progress_cb(local_path, remote_path, 100, true); - } + if (progress_cb) { + progress_cb(local_path, remote_path, 100, true); + } - await cleanup(dst_fd); - return true; + await cleanup(dst_fd); + return true; + } catch (err) { + await cleanup(dst_fd); + return Promise.reject(new Error(`'download_file' failed: ${err}`)); + } } catch (err) { - await cleanup(dst_fd); + await cleanup(); return Promise.reject(new Error(`'download_file' failed: ${err}`)); } } catch (err) { - await cleanup(); return Promise.reject(new Error(`'download_file' failed: ${err}`)); } - } catch (err) { - return Promise.reject(new Error(`'download_file' failed: ${err}`)); - } -}; + }; export const get_drive_information = async conn => { try { const response = - await conn.send('::RemoteWinFSPGetVolumeInfo', new packet()); + await conn.send('::RemoteWinFSPGetVolumeInfo', new packet()); response.decode_ui32(); // Service flags const result = response.decode_i32(); @@ -173,86 +236,49 @@ export const get_drive_information = async conn => { return { free, total, - used : (new Uint64BE(total) - new Uint64BE(free)).toString(10), + used: (new Uint64BE(total) - new Uint64BE(free)).toString(10), }; } return Promise.reject( - new Error(`'get_drive_information' failed: ${result}`)); + new Error(`'get_drive_information' failed: ${result}`)); } catch (err) { return Promise.reject(new Error(`'get_drive_information' failed: ${err}`)); } }; export const get_file_attributes = - async (conn, handle, remote_path, optional_thread_id) => { - try { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui64(handle); - request.encode_ui32(0); - request.encode_ui32(0); + async (conn, handle, remote_path, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(handle); + request.encode_ui32(0); + request.encode_ui32(0); - const response = + const response = await conn.send('::RemoteFUSEFgetattr', request, optional_thread_id); - response.decode_ui32(); // Service flags + response.decode_ui32(); // Service flags - const result = response.decode_i32(); - if (result === 0) { - return response.decode_stat(); + const result = response.decode_i32(); + if (result === 0) { + return response.decode_stat(); + } + + return Promise.reject(new Error(`'get_file_attributes' failed: ${result}`)); + } catch (err) { + return Promise.reject(new Error(`'get_file_attributes' failed: ${err}`)); } - - return Promise.reject(new Error(`'get_file_attributes' failed: ${result}`)); - } catch (err) { - return Promise.reject(new Error(`'get_file_attributes' failed: ${err}`)); - } -}; + }; export const list_directory = async (conn, remote_path, page_reader_cb) => { + const dir_snapshot = await _snapshot_directory(conn, remote_path); try { - const request = new packet(); - request.encode_utf8(remote_path); - - const response = - await conn.send('::RemoteJSONCreateDirectorySnapshot', request); - response.decode_ui32(); // Service flags - - const result = response.decode_i32(); - if (result === 0) { - const data = JSON.parse(response.decode_utf8()); - const cleanup = async () => { - const request = new packet(); - request.encode_ui64(data.handle); - await conn.send('::RemoteJSONReleaseDirectorySnapshot', request); - }; - try { - const get_page = async page => { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui64(data.handle); - request.encode_ui32(page); - - const response = - await conn.send('::RemoteJSONReadDirectorySnapshot', request); - response.decode_ui32(); // Service flags - - const result = response.decode_i32(); - if (result === 0 || result === -120) { - const data = JSON.parse(response.decode_utf8()); - return data.directory_list; - } - return []; - }; - - await page_reader_cb(remote_path, data.page_count, get_page); - await cleanup(); - } catch (err) { - await cleanup(); - return Promise.reject(new Error(`'list_directory' failed: ${err}`)); - } - } + await page_reader_cb(dir_snapshot.remote_path, dir_snapshot.page_count, dir_snapshot.get_page); + await dir_snapshot.release(); } catch (err) { - return Promise.reject(new Error(`'list_directory' failed: ${err}`)); + await dir_snapshot.release(); + return Promise.reject(`'list_directory' failed: ${err}`); } }; @@ -263,7 +289,7 @@ export const open_file = async (conn, remote_path, optional_thread_id) => { request.encode_ui32(2); // Read-Write const response = - await conn.send('::RemoteFUSEOpen', request, optional_thread_id); + await conn.send('::RemoteFUSEOpen', request, optional_thread_id); response.decode_ui32(); // Service flags const result = response.decode_i32(); @@ -277,27 +303,27 @@ export const open_file = async (conn, remote_path, optional_thread_id) => { }; export const read_file = - async (conn, handle, remote_path, offset, length, optional_thread_id) => { - try { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui64(length); - request.encode_ui64(offset); - request.encode_ui64(handle); + async (conn, handle, remote_path, offset, length, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(length); + request.encode_ui64(offset); + request.encode_ui64(handle); - const response = + const response = await conn.send('::RemoteFUSERead', request, optional_thread_id); - response.decode_ui32(); // Service flags + response.decode_ui32(); // Service flags - const result = response.decode_i32(); - if (result === length) { - return response.decode_buffer(result); + const result = response.decode_i32(); + if (result === length) { + return response.decode_buffer(result); + } + return Promise.reject(new Error(`'read_file' error: ${result}`)); + } catch (err) { + return Promise.reject(new Error(`'read_file' failed: ${err}`)); } - return Promise.reject(new Error(`'read_file' error: ${result}`)); - } catch (err) { - return Promise.reject(new Error(`'read_file' failed: ${err}`)); - } -}; + }; export const remove_directory = async (conn, remote_path) => { try { @@ -313,146 +339,148 @@ export const remove_directory = async (conn, remote_path) => { } }; +export const snapshot_directory = _snapshot_directory; + export const truncate_file = - async (conn, handle, remote_path, length, optional_thread_id) => { - try { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui64(length); - request.encode_ui64(handle); + async (conn, handle, remote_path, length, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(length); + request.encode_ui64(handle); - const response = + const response = await conn.send('::RemoteFUSEFtruncate', request, optional_thread_id); - response.decode_ui32(); // Service flags + response.decode_ui32(); // Service flags - return response.decode_i32(); - } catch (err) { - return Promise.reject(new Error(`'truncate_file' failed: ${err}`)); - } -}; + return response.decode_i32(); + } catch (err) { + return Promise.reject(new Error(`'truncate_file' failed: ${err}`)); + } + }; export const upload_file = - async (conn, local_path, remote_path, progress_cb, overwrite, resume) => { - try { - const src_fd = fs.openSync(local_path, 'r'); - const cleanup = async f => { - try { - fs.closeSync(src_fd); - } catch (err) { - console.log(err); - } - try { - if (f) { - await f.close(); - } - } catch (err) { - console.log(err); - } - }; + async (conn, local_path, remote_path, progress_cb, overwrite, resume) => { try { - const src_st = fs.fstatSync(src_fd); - let dst; - const create_dest = async () => { - dst = new file(conn, await create_or_open_file(conn, remote_path), - remote_path); - }; - - try { - let offset = 0; - if (overwrite) { - await create_dest(); - const result = await dst.truncate(0); - if (result !== 0) { - await cleanup(dst); - return Promise.reject(new Error(`'upload_file' failed: ${result}`)); - } - } else if (resume) { - await create_dest(); - const dst_size = new Uint64BE(await dst.get_size()).toNumber(); - if (dst_size === src_st.size) { - await cleanup(dst); - return true; - } - - if (dst_size > src_st.size) { - await cleanup(dst); - return Promise.reject(new Error( - `'upload_file' failed: destination is larger than source`)); - } - - offset = dst_size; - } else { - try { - const f = - new file(conn, await open_file(conn, remote_path), remote_path); - await cleanup(f); - return Promise.reject( - new Error("'upload_file' failed: file exists")); - } catch (err) { - await create_dest(); - } + const src_fd = fs.openSync(local_path, 'r'); + const cleanup = async f => { + try { + fs.closeSync(src_fd); + } catch (err) { + console.log(err); } + try { + if (f) { + await f.close(); + } + } catch (err) { + console.log(err); + } + }; + try { + const src_st = fs.fstatSync(src_fd); + let dst; + const create_dest = async () => { + dst = new file(conn, await create_or_open_file(conn, remote_path), + remote_path); + }; - let remain = src_st.size - offset; - const default_buffer = Buffer.alloc(65536 * 2); - while (remain > 0) { - const to_write = - remain >= default_buffer.length ? default_buffer.length : remain; - const buffer = to_write === default_buffer.length - ? default_buffer - : Buffer.alloc(to_write); - fs.readSync(src_fd, buffer, 0, to_write, offset); - const written = await dst.write(offset, buffer); - if (written > 0) { - remain -= written; - offset += written; - if (progress_cb) { - progress_cb(local_path, remote_path, - ((src_st.size - remain) / src_st.size) * 100.0, - false); + try { + let offset = 0; + if (overwrite) { + await create_dest(); + const result = await dst.truncate(0); + if (result !== 0) { + await cleanup(dst); + return Promise.reject(new Error(`'upload_file' failed: ${result}`)); + } + } else if (resume) { + await create_dest(); + const dst_size = new Uint64BE(await dst.get_size()).toNumber(); + if (dst_size === src_st.size) { + await cleanup(dst); + return true; + } + + if (dst_size > src_st.size) { + await cleanup(dst); + return Promise.reject(new Error( + `'upload_file' failed: destination is larger than source`)); + } + + offset = dst_size; + } else { + try { + const f = + new file(conn, await open_file(conn, remote_path), remote_path); + await cleanup(f); + return Promise.reject( + new Error('\'upload_file\' failed: file exists')); + } catch (err) { + await create_dest(); } } - } - if (progress_cb) { - progress_cb(local_path, remote_path, 100, true); - } + let remain = src_st.size - offset; + const default_buffer = Buffer.alloc(65536 * 2); + while (remain > 0) { + const to_write = + remain >= default_buffer.length ? default_buffer.length : remain; + const buffer = to_write === default_buffer.length + ? default_buffer + : Buffer.alloc(to_write); + fs.readSync(src_fd, buffer, 0, to_write, offset); + const written = await dst.write(offset, buffer); + if (written > 0) { + remain -= written; + offset += written; + if (progress_cb) { + progress_cb(local_path, remote_path, + ((src_st.size - remain) / src_st.size) * 100.0, + false); + } + } + } - await cleanup(dst); - return true; + if (progress_cb) { + progress_cb(local_path, remote_path, 100, true); + } + + await cleanup(dst); + return true; + } catch (err) { + await cleanup(dst); + return Promise.reject(new Error(`'upload_file' failed: ${err}`)); + } } catch (err) { - await cleanup(dst); + await cleanup(); return Promise.reject(new Error(`'upload_file' failed: ${err}`)); } } catch (err) { - await cleanup(); return Promise.reject(new Error(`'upload_file' failed: ${err}`)); } - } catch (err) { - return Promise.reject(new Error(`'upload_file' failed: ${err}`)); - } -}; + }; export const write_file = - async (conn, handle, remote_path, offset, buffer, optional_thread_id) => { - try { - const request = new packet(); - request.encode_utf8(remote_path); - request.encode_ui64(buffer.length); - request.encode_buffer(buffer); - request.encode_ui64(offset); - request.encode_ui64(handle); + async (conn, handle, remote_path, offset, buffer, optional_thread_id) => { + try { + const request = new packet(); + request.encode_utf8(remote_path); + request.encode_ui64(buffer.length); + request.encode_buffer(buffer); + request.encode_ui64(offset); + request.encode_ui64(handle); - const response = + const response = await conn.send('::RemoteFUSEWrite', request, optional_thread_id); - response.decode_ui32(); // Service flags + response.decode_ui32(); // Service flags - const result = response.decode_i32(); - if (result === buffer.length) { - return result; + const result = response.decode_i32(); + if (result === buffer.length) { + return result; + } + return Promise.reject(new Error(`'write_file' error: ${result}`)); + } catch (err) { + return Promise.reject(new Error(`'write_file' failed: ${err}`)); } - return Promise.reject(new Error(`'write_file' error: ${result}`)); - } catch (err) { - return Promise.reject(new Error(`'write_file' failed: ${err}`)); - } -}; + };