feat(cli): add private NanoShare sync client
Build and Push Docker Container / build-and-push (push) Successful in 1m3s
Build and Push Docker Container / build-and-push (push) Successful in 1m3s
- Add a separate installable NanoShare CLI package under cli/. - Implement browser login with local callback and refresh-token config. - Add upload, list, download, sync, and watch CLI commands. - Add private owner-only file download endpoint for CLI downloads. - Add CLI auth endpoints for browser login and token refresh. - Return file_id from ServiceLink uploads for reliable sync state. - Exclude the CLI package from NanoShare container builds. - Include tests for private downloads and sync state updates.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'cli'))
|
||||
|
||||
from nanoshare_client import cli
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self):
|
||||
self.registry = {'picoshare': 'https://example.com'}
|
||||
self.uploads = []
|
||||
self.deleted = []
|
||||
self.remote_files = []
|
||||
|
||||
def call(self, node, method, params):
|
||||
if method == 'files.list':
|
||||
return {'files': list(self.remote_files)}
|
||||
if method == 'files.upload':
|
||||
file_id = f'file_{len(self.uploads) + 1}'
|
||||
self.uploads.append((node, params, file_id))
|
||||
self.remote_files.append({
|
||||
'file_id': file_id,
|
||||
'file_name': params['file_name'],
|
||||
'file_size': '1 B',
|
||||
})
|
||||
return {'file_id': file_id, 'file_name': params['file_name']}
|
||||
if method == 'files.delete':
|
||||
self.deleted.append(params['file_id'])
|
||||
self.remote_files = [item for item in self.remote_files if item['file_id'] != params['file_id']]
|
||||
return {'deleted': True}
|
||||
raise AssertionError(method)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def test_sync_uploads_new_local_file_and_writes_state(tmp_path, monkeypatch):
|
||||
fake = FakeClient()
|
||||
monkeypatch.setattr(cli, 'make_client', lambda args: fake)
|
||||
|
||||
(tmp_path / 'hello.txt').write_text('hello')
|
||||
|
||||
code = cli.main([
|
||||
'sync',
|
||||
'--url', 'picoshare=https://example.com',
|
||||
'--token', 'token',
|
||||
str(tmp_path),
|
||||
])
|
||||
|
||||
assert code == 0
|
||||
assert fake.uploads[0][1]['file_name'] == 'hello.txt'
|
||||
state = json.loads((tmp_path / '.nanoshare-sync' / 'state.json').read_text())
|
||||
assert state['files']['hello.txt']['file_id'] == 'file_1'
|
||||
|
||||
def test_sync_deletes_old_remote_when_local_file_changes(tmp_path, monkeypatch):
|
||||
fake = FakeClient()
|
||||
fake.remote_files = [{'file_id': 'old_file', 'file_name': 'hello.txt', 'file_size': '5 B'}]
|
||||
monkeypatch.setattr(cli, 'make_client', lambda args: fake)
|
||||
|
||||
path = tmp_path / 'hello.txt'
|
||||
path.write_text('changed')
|
||||
state_dir = tmp_path / '.nanoshare-sync'
|
||||
state_dir.mkdir()
|
||||
(state_dir / 'state.json').write_text(json.dumps({
|
||||
'version': 1,
|
||||
'files': {
|
||||
'hello.txt': {
|
||||
'file_id': 'old_file',
|
||||
'sha256': 'old_hash',
|
||||
'local_mtime': 1,
|
||||
'file_name': 'hello.txt',
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
code = cli.main([
|
||||
'sync',
|
||||
'--url', 'picoshare=https://example.com',
|
||||
'--token', 'token',
|
||||
str(tmp_path),
|
||||
])
|
||||
|
||||
assert code == 0
|
||||
assert fake.deleted == ['old_file']
|
||||
state = json.loads((state_dir / 'state.json').read_text())
|
||||
assert state['files']['hello.txt']['file_id'] == 'file_1'
|
||||
Reference in New Issue
Block a user