cloud-tc 0.1.0__tar.gz
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.
- cloud_tc-0.1.0/LICENSE +21 -0
- cloud_tc-0.1.0/PKG-INFO +157 -0
- cloud_tc-0.1.0/README.md +121 -0
- cloud_tc-0.1.0/cloud_tc/__init__.py +1 -0
- cloud_tc-0.1.0/cloud_tc/cli.py +422 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/PKG-INFO +157 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/SOURCES.txt +11 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/dependency_links.txt +1 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/entry_points.txt +3 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/requires.txt +3 -0
- cloud_tc-0.1.0/cloud_tc.egg-info/top_level.txt +1 -0
- cloud_tc-0.1.0/pyproject.toml +51 -0
- cloud_tc-0.1.0/setup.cfg +4 -0
cloud_tc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OnlySq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cloud_tc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloud-tc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal client for OnlySq Cloud 2.0 — talks to https://cloud.onlysq.ru via REST
|
|
5
|
+
Author-email: OnlySq <contact@onlysq.ru>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/xOnlySq/cloud-tc
|
|
8
|
+
Project-URL: Repository, https://github.com/xOnlySq/cloud-tc
|
|
9
|
+
Project-URL: Issues, https://github.com/xOnlySq/cloud-tc/issues
|
|
10
|
+
Project-URL: Service, https://cloud.onlysq.ru
|
|
11
|
+
Project-URL: Documentation, https://cloud.onlysq.ru/docs
|
|
12
|
+
Keywords: onlysq,cloud,telegram,cli,storage,drive
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Operating System :: MacOS
|
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: System :: Archiving
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: httpx>=0.27
|
|
33
|
+
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: rich>=13.7
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# cloud-tc
|
|
38
|
+
|
|
39
|
+
Terminal client for **OnlySq Cloud 2.0** (`https://cloud.onlysq.ru`). Talks to the public v2 REST API — no Telegram, no Postgres on your machine, just `httpx` + `typer` + `rich`.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
From PyPI (when published):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install cloud-tc
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Straight from git:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install git+https://github.com/xOnlySq/cloud-tc.git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or for a local checkout (with `-e` for editable):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/xOnlySq/cloud-tc.git
|
|
59
|
+
cd cloud-tc
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both `tc` and `cloud-tc` console scripts are installed.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
1. Create an API token in the web UI: `https://cloud.onlysq.ru/v2/ui/settings` → "new token".
|
|
68
|
+
2. Log in locally:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
tc login --token tck_xxxxxxxxxxxxxxxxxxxx
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The token is stored in `~/.cloud-tc/config.json` (0600).
|
|
75
|
+
|
|
76
|
+
3. Use it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
tc me
|
|
80
|
+
tc quota
|
|
81
|
+
tc ls
|
|
82
|
+
tc upload ./photo.jpg
|
|
83
|
+
tc share <uid> --role viewer
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Commands
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `tc login --token tck_…` | Save and verify an API token |
|
|
91
|
+
| `tc logout` | Remove local config |
|
|
92
|
+
| `tc info` | Show local config |
|
|
93
|
+
| `tc version` | Print package version |
|
|
94
|
+
| `tc me` | Show current user profile |
|
|
95
|
+
| `tc quota` | Show used / total quota |
|
|
96
|
+
| `tc ls [FOLDER_ID]` | List files |
|
|
97
|
+
| `tc upload PATH [-f FOLDER]` | Upload a local file |
|
|
98
|
+
| `tc download UID [-o PATH]` | Download a file |
|
|
99
|
+
| `tc rm UID` | Move to trash |
|
|
100
|
+
| `tc restore UID` | Restore from trash |
|
|
101
|
+
| `tc mv UID FOLDER_ID` | Move file to another folder |
|
|
102
|
+
| `tc rename UID NEW_NAME` | Rename |
|
|
103
|
+
| `tc mkdir NAME [-p PARENT_ID]` | Create folder |
|
|
104
|
+
| `tc share UID [-r ROLE] [--password] [--expires] [--max-uses]` | Create a share link |
|
|
105
|
+
| `tc tokens list / create NAME / revoke ID` | Manage API tokens |
|
|
106
|
+
| `tc migrate-self LEGACY_TOKEN` | Move a legacy account to v2 (no auth required) |
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
You can override the server with environment variables (useful for self-hosted instances):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export TC_BASE_URL="https://cloud.example.com"
|
|
114
|
+
export TC_TOKEN="tck_..."
|
|
115
|
+
tc ls
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`TC_TOKEN` overrides the saved token if set. `TC_BASE_URL` overrides the configured base URL.
|
|
119
|
+
|
|
120
|
+
Config file: `~/.cloud-tc/config.json` — just `{ "token": "...", "base_url": "..." }`.
|
|
121
|
+
|
|
122
|
+
## Examples
|
|
123
|
+
|
|
124
|
+
Upload everything from a directory into a folder:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
for f in *.jpg; do tc upload "$f" -f 42; done
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Pipe a file through `tc upload` (Linux):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
tc upload /dev/stdin <<< "hello" # named pipe trick, see issues for binary
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Quick public link for a file:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
UID=$(tc upload ./report.pdf | awk '{print $2}')
|
|
140
|
+
tc share "$UID" --role viewer
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Batch-migrate one legacy token from any script:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
tc migrate-self USERS_LEGACY_TOKEN
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT. See [LICENSE](LICENSE).
|
|
152
|
+
|
|
153
|
+
## Related
|
|
154
|
+
|
|
155
|
+
- Cloud UI: <https://cloud.onlysq.ru>
|
|
156
|
+
- REST API docs: <https://cloud.onlysq.ru/docs> (see "Cloud 2.0" group)
|
|
157
|
+
- Service / issues: <https://github.com/xOnlySq/cloud-tc/issues>
|
cloud_tc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# cloud-tc
|
|
2
|
+
|
|
3
|
+
Terminal client for **OnlySq Cloud 2.0** (`https://cloud.onlysq.ru`). Talks to the public v2 REST API — no Telegram, no Postgres on your machine, just `httpx` + `typer` + `rich`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From PyPI (when published):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install cloud-tc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Straight from git:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install git+https://github.com/xOnlySq/cloud-tc.git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or for a local checkout (with `-e` for editable):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/xOnlySq/cloud-tc.git
|
|
23
|
+
cd cloud-tc
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Both `tc` and `cloud-tc` console scripts are installed.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
1. Create an API token in the web UI: `https://cloud.onlysq.ru/v2/ui/settings` → "new token".
|
|
32
|
+
2. Log in locally:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
tc login --token tck_xxxxxxxxxxxxxxxxxxxx
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The token is stored in `~/.cloud-tc/config.json` (0600).
|
|
39
|
+
|
|
40
|
+
3. Use it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
tc me
|
|
44
|
+
tc quota
|
|
45
|
+
tc ls
|
|
46
|
+
tc upload ./photo.jpg
|
|
47
|
+
tc share <uid> --role viewer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `tc login --token tck_…` | Save and verify an API token |
|
|
55
|
+
| `tc logout` | Remove local config |
|
|
56
|
+
| `tc info` | Show local config |
|
|
57
|
+
| `tc version` | Print package version |
|
|
58
|
+
| `tc me` | Show current user profile |
|
|
59
|
+
| `tc quota` | Show used / total quota |
|
|
60
|
+
| `tc ls [FOLDER_ID]` | List files |
|
|
61
|
+
| `tc upload PATH [-f FOLDER]` | Upload a local file |
|
|
62
|
+
| `tc download UID [-o PATH]` | Download a file |
|
|
63
|
+
| `tc rm UID` | Move to trash |
|
|
64
|
+
| `tc restore UID` | Restore from trash |
|
|
65
|
+
| `tc mv UID FOLDER_ID` | Move file to another folder |
|
|
66
|
+
| `tc rename UID NEW_NAME` | Rename |
|
|
67
|
+
| `tc mkdir NAME [-p PARENT_ID]` | Create folder |
|
|
68
|
+
| `tc share UID [-r ROLE] [--password] [--expires] [--max-uses]` | Create a share link |
|
|
69
|
+
| `tc tokens list / create NAME / revoke ID` | Manage API tokens |
|
|
70
|
+
| `tc migrate-self LEGACY_TOKEN` | Move a legacy account to v2 (no auth required) |
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
You can override the server with environment variables (useful for self-hosted instances):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export TC_BASE_URL="https://cloud.example.com"
|
|
78
|
+
export TC_TOKEN="tck_..."
|
|
79
|
+
tc ls
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`TC_TOKEN` overrides the saved token if set. `TC_BASE_URL` overrides the configured base URL.
|
|
83
|
+
|
|
84
|
+
Config file: `~/.cloud-tc/config.json` — just `{ "token": "...", "base_url": "..." }`.
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
Upload everything from a directory into a folder:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
for f in *.jpg; do tc upload "$f" -f 42; done
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Pipe a file through `tc upload` (Linux):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
tc upload /dev/stdin <<< "hello" # named pipe trick, see issues for binary
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Quick public link for a file:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
UID=$(tc upload ./report.pdf | awk '{print $2}')
|
|
104
|
+
tc share "$UID" --role viewer
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Batch-migrate one legacy token from any script:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
tc migrate-self USERS_LEGACY_TOKEN
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT. See [LICENSE](LICENSE).
|
|
116
|
+
|
|
117
|
+
## Related
|
|
118
|
+
|
|
119
|
+
- Cloud UI: <https://cloud.onlysq.ru>
|
|
120
|
+
- REST API docs: <https://cloud.onlysq.ru/docs> (see "Cloud 2.0" group)
|
|
121
|
+
- Service / issues: <https://github.com/xOnlySq/cloud-tc/issues>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from cloud_tc import __version__
|
|
16
|
+
|
|
17
|
+
IS_WINDOWS = sys.platform.startswith("win")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _enable_windows_ansi() -> None:
|
|
21
|
+
if not IS_WINDOWS:
|
|
22
|
+
return
|
|
23
|
+
try:
|
|
24
|
+
import ctypes
|
|
25
|
+
kernel32 = ctypes.windll.kernel32
|
|
26
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
27
|
+
for handle_id in (-11, -12):
|
|
28
|
+
h = kernel32.GetStdHandle(handle_id)
|
|
29
|
+
mode = ctypes.c_uint32()
|
|
30
|
+
if kernel32.GetConsoleMode(h, ctypes.byref(mode)):
|
|
31
|
+
kernel32.SetConsoleMode(h, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_enable_windows_ansi()
|
|
37
|
+
|
|
38
|
+
app = typer.Typer(
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
add_completion=False,
|
|
41
|
+
pretty_exceptions_show_locals=False,
|
|
42
|
+
help="Terminal client for OnlySq Cloud 2.0",
|
|
43
|
+
)
|
|
44
|
+
console = Console()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _config_dir() -> Path:
|
|
48
|
+
"""
|
|
49
|
+
Cross-platform per-user config directory.
|
|
50
|
+
|
|
51
|
+
Windows: %APPDATA%\\cloud-tc
|
|
52
|
+
macOS: ~/Library/Application Support/cloud-tc
|
|
53
|
+
Linux/*: $XDG_CONFIG_HOME/cloud-tc, fallback ~/.config/cloud-tc
|
|
54
|
+
|
|
55
|
+
Legacy ~/.cloud-tc (used by 0.1.0) is honored if it already exists.
|
|
56
|
+
"""
|
|
57
|
+
override = os.environ.get("CLOUD_TC_HOME")
|
|
58
|
+
if override:
|
|
59
|
+
return Path(override).expanduser()
|
|
60
|
+
|
|
61
|
+
legacy = Path.home() / ".cloud-tc"
|
|
62
|
+
if legacy.exists():
|
|
63
|
+
return legacy
|
|
64
|
+
|
|
65
|
+
if IS_WINDOWS:
|
|
66
|
+
base = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
|
|
67
|
+
return Path(base) / "cloud-tc"
|
|
68
|
+
if sys.platform == "darwin":
|
|
69
|
+
return Path.home() / "Library" / "Application Support" / "cloud-tc"
|
|
70
|
+
xdg = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
71
|
+
return Path(xdg) / "cloud-tc"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
CONFIG_DIR = _config_dir()
|
|
75
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
76
|
+
DEFAULT_BASE = "https://cloud.onlysq.ru"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _save(data: dict) -> None:
|
|
80
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
82
|
+
if not IS_WINDOWS:
|
|
83
|
+
try:
|
|
84
|
+
CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _load() -> dict:
|
|
90
|
+
if not CONFIG_FILE.exists():
|
|
91
|
+
return {}
|
|
92
|
+
try:
|
|
93
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
94
|
+
except Exception:
|
|
95
|
+
return {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _client(require_auth: bool = True) -> httpx.Client:
|
|
99
|
+
cfg = _load()
|
|
100
|
+
base = cfg.get("base_url") or os.environ.get("TC_BASE_URL") or DEFAULT_BASE
|
|
101
|
+
token = cfg.get("token") or os.environ.get("TC_TOKEN")
|
|
102
|
+
headers = {"User-Agent": f"cloud-tc/{__version__} ({sys.platform})"}
|
|
103
|
+
if token:
|
|
104
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
105
|
+
elif require_auth:
|
|
106
|
+
console.print("[red]no token configured[/] · run [bold]tc login --token tck_…[/]")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
return httpx.Client(base_url=base, headers=headers, timeout=120.0, follow_redirects=False)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _hsize(n) -> str:
|
|
112
|
+
n = int(n or 0)
|
|
113
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
114
|
+
if n >= 1024 ** 6:
|
|
115
|
+
return "∞"
|
|
116
|
+
i = 0
|
|
117
|
+
f = float(n)
|
|
118
|
+
while f >= 1024 and i < len(units) - 1:
|
|
119
|
+
f /= 1024
|
|
120
|
+
i += 1
|
|
121
|
+
if i == 0:
|
|
122
|
+
return f"{int(f)} {units[i]}"
|
|
123
|
+
if i == 1:
|
|
124
|
+
return f"{f:.1f} {units[i]}"
|
|
125
|
+
return f"{f:.2f} {units[i]}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _fail_if_bad(r: httpx.Response, ctx: str = "") -> dict:
|
|
129
|
+
try:
|
|
130
|
+
data = r.json()
|
|
131
|
+
except Exception:
|
|
132
|
+
data = {"raw": r.text}
|
|
133
|
+
if r.status_code >= 300 or (isinstance(data, dict) and data.get("ok") is False):
|
|
134
|
+
msg = data.get("error") if isinstance(data, dict) else r.text
|
|
135
|
+
console.print(f"[red]{ctx or 'error'}[/] HTTP {r.status_code} · {msg}")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def version():
|
|
142
|
+
console.print(f"cloud-tc [bold]{__version__}[/] · {sys.platform} · python {sys.version.split()[0]}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command()
|
|
146
|
+
def info():
|
|
147
|
+
cfg = _load()
|
|
148
|
+
console.print(f"[bold]cloud-tc[/] v{__version__}")
|
|
149
|
+
console.print(f" platform: {sys.platform}")
|
|
150
|
+
console.print(f" config: {CONFIG_FILE}")
|
|
151
|
+
if cfg:
|
|
152
|
+
console.print(f" base_url = {cfg.get('base_url', DEFAULT_BASE)}")
|
|
153
|
+
if cfg.get("token"):
|
|
154
|
+
console.print(f" token = {cfg['token'][:14]}…")
|
|
155
|
+
else:
|
|
156
|
+
console.print(" [dim]not logged in[/]")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def login(
|
|
161
|
+
token: str = typer.Option(..., "--token", "-t", help="API token (tck_…) — create one at /v2/ui/settings"),
|
|
162
|
+
base_url: str = typer.Option(DEFAULT_BASE, "--base-url", "-u"),
|
|
163
|
+
):
|
|
164
|
+
"""Save an API token locally and verify it works."""
|
|
165
|
+
with httpx.Client(base_url=base_url, headers={"Authorization": f"Bearer {token}"}, timeout=30) as cli:
|
|
166
|
+
r = cli.get("/v2/quota")
|
|
167
|
+
if r.status_code != 200:
|
|
168
|
+
console.print(f"[red]token rejected[/] HTTP {r.status_code} · {r.text}")
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
q = r.json()
|
|
171
|
+
_save({"token": token, "base_url": base_url})
|
|
172
|
+
console.print(
|
|
173
|
+
f"[green]ok[/] logged in · used {_hsize(q['used_bytes'])} / "
|
|
174
|
+
f"{_hsize(q['quota_bytes'])}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def logout():
|
|
180
|
+
"""Remove the local config."""
|
|
181
|
+
if CONFIG_FILE.exists():
|
|
182
|
+
CONFIG_FILE.unlink()
|
|
183
|
+
console.print("[green]ok[/] config removed")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def me():
|
|
188
|
+
"""Print the current user profile."""
|
|
189
|
+
with _client() as cli:
|
|
190
|
+
r = cli.get("/auth/me")
|
|
191
|
+
data = _fail_if_bad(r, "me")
|
|
192
|
+
u = data.get("user") or {}
|
|
193
|
+
console.print(f"[bold]{u.get('name')}[/] · {u.get('email') or 'no email'}")
|
|
194
|
+
console.print(f" sauth_id: {u.get('sauth_id')} level: {u.get('level')}{' · admin' if u.get('is_admin') else ''}")
|
|
195
|
+
console.print(f" storage: {_hsize(u.get('used_bytes'))} / {_hsize(u.get('quota_bytes'))}")
|
|
196
|
+
if u.get("has_legacy"):
|
|
197
|
+
console.print(f" legacy: {'migrated' if u.get('migrated') else 'linked, not migrated'}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command()
|
|
201
|
+
def quota():
|
|
202
|
+
"""Show used / total quota."""
|
|
203
|
+
with _client() as cli:
|
|
204
|
+
r = cli.get("/v2/quota")
|
|
205
|
+
q = _fail_if_bad(r, "quota")
|
|
206
|
+
used = q["used_bytes"]
|
|
207
|
+
total = q["quota_bytes"]
|
|
208
|
+
pct = (used / max(1, total)) * 100
|
|
209
|
+
if total >= 1024 ** 6:
|
|
210
|
+
console.print(f"used [bold]{_hsize(used)}[/] / ∞ (unlimited tier)")
|
|
211
|
+
else:
|
|
212
|
+
console.print(f"used [bold]{_hsize(used)}[/] / {_hsize(total)} ([yellow]{pct:.1f}%[/])")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command()
|
|
216
|
+
def ls(folder_id: Optional[int] = typer.Argument(None, help="folder id (omit for root)")):
|
|
217
|
+
"""List files in a folder."""
|
|
218
|
+
with _client() as cli:
|
|
219
|
+
q = f"?folder_id={folder_id}" if folder_id else "?root=1"
|
|
220
|
+
r = cli.get(f"/v2/files{q}")
|
|
221
|
+
data = _fail_if_bad(r, "list")
|
|
222
|
+
files = data.get("files", [])
|
|
223
|
+
if not files:
|
|
224
|
+
console.print("[dim]empty[/]")
|
|
225
|
+
return
|
|
226
|
+
t = Table(show_lines=False, header_style="bold")
|
|
227
|
+
t.add_column("UID", style="cyan", no_wrap=True)
|
|
228
|
+
t.add_column("Name")
|
|
229
|
+
t.add_column("Size", justify="right", style="dim")
|
|
230
|
+
t.add_column("Mime", style="dim")
|
|
231
|
+
t.add_column("Status", justify="center")
|
|
232
|
+
for f in files:
|
|
233
|
+
status = "ready" if f.get("status") == 1 else f"#{f.get('status')}"
|
|
234
|
+
t.add_row(
|
|
235
|
+
f["uid"], f["name"], _hsize(f.get("size")),
|
|
236
|
+
f.get("mime") or "—", status,
|
|
237
|
+
)
|
|
238
|
+
console.print(t)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command()
|
|
242
|
+
def upload(
|
|
243
|
+
path: Path = typer.Argument(..., exists=True, readable=True),
|
|
244
|
+
folder_id: Optional[int] = typer.Option(None, "--folder", "-f"),
|
|
245
|
+
):
|
|
246
|
+
"""Upload a local file."""
|
|
247
|
+
path = path.resolve()
|
|
248
|
+
if not path.is_file():
|
|
249
|
+
console.print("[red]not a file[/]")
|
|
250
|
+
raise typer.Exit(1)
|
|
251
|
+
with _client() as cli:
|
|
252
|
+
with path.open("rb") as f:
|
|
253
|
+
files = {"file": (path.name, f, "application/octet-stream")}
|
|
254
|
+
data = {"folder_id": str(folder_id)} if folder_id else {}
|
|
255
|
+
r = cli.post("/v2/files/upload", files=files, data=data)
|
|
256
|
+
out = _fail_if_bad(r, "upload")
|
|
257
|
+
f = out["file"]
|
|
258
|
+
console.print(f"[green]ok[/] {f['uid']} · {f['name']} · {_hsize(f['size'])}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def download(
|
|
263
|
+
uid: str = typer.Argument(...),
|
|
264
|
+
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
|
265
|
+
):
|
|
266
|
+
"""Download a file by uid."""
|
|
267
|
+
with _client() as cli:
|
|
268
|
+
meta = _fail_if_bad(cli.get(f"/v2/files/{uid}"), "meta")
|
|
269
|
+
name = meta["file"]["name"]
|
|
270
|
+
target = (out or Path(name)).resolve()
|
|
271
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
with cli.stream("GET", f"/v2/files/{uid}/stream?mode=dl") as r:
|
|
273
|
+
if r.status_code != 200:
|
|
274
|
+
console.print(f"[red]error[/] HTTP {r.status_code}")
|
|
275
|
+
raise typer.Exit(1)
|
|
276
|
+
with target.open("wb") as fp:
|
|
277
|
+
for chunk in r.iter_bytes(chunk_size=65536):
|
|
278
|
+
fp.write(chunk)
|
|
279
|
+
console.print(f"[green]ok[/] saved {target}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@app.command()
|
|
283
|
+
def rm(uid: str = typer.Argument(...)):
|
|
284
|
+
"""Soft-delete a file (moves to trash for 30 days)."""
|
|
285
|
+
with _client() as cli:
|
|
286
|
+
_fail_if_bad(cli.delete(f"/v2/files/{uid}"), "delete")
|
|
287
|
+
console.print(f"[green]ok[/] {uid} → trash")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@app.command()
|
|
291
|
+
def restore(uid: str = typer.Argument(...)):
|
|
292
|
+
"""Restore a file from trash."""
|
|
293
|
+
with _client() as cli:
|
|
294
|
+
_fail_if_bad(cli.post(f"/v2/files/{uid}/restore"), "restore")
|
|
295
|
+
console.print(f"[green]ok[/] {uid} restored")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.command()
|
|
299
|
+
def mv(
|
|
300
|
+
uid: str = typer.Argument(...),
|
|
301
|
+
folder_id: int = typer.Argument(...),
|
|
302
|
+
):
|
|
303
|
+
"""Move a file to another folder."""
|
|
304
|
+
with _client() as cli:
|
|
305
|
+
_fail_if_bad(cli.patch(f"/v2/files/{uid}", json={"folder_id": folder_id}), "move")
|
|
306
|
+
console.print(f"[green]ok[/] {uid} → folder #{folder_id}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.command()
|
|
310
|
+
def rename(
|
|
311
|
+
uid: str = typer.Argument(...),
|
|
312
|
+
name: str = typer.Argument(...),
|
|
313
|
+
):
|
|
314
|
+
"""Rename a file."""
|
|
315
|
+
with _client() as cli:
|
|
316
|
+
_fail_if_bad(cli.patch(f"/v2/files/{uid}", json={"name": name}), "rename")
|
|
317
|
+
console.print(f"[green]ok[/] renamed to {name}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.command()
|
|
321
|
+
def mkdir(
|
|
322
|
+
name: str = typer.Argument(...),
|
|
323
|
+
parent: Optional[int] = typer.Option(None, "--parent", "-p"),
|
|
324
|
+
):
|
|
325
|
+
"""Create a folder."""
|
|
326
|
+
with _client() as cli:
|
|
327
|
+
body: dict = {"name": name}
|
|
328
|
+
if parent is not None:
|
|
329
|
+
body["parent_id"] = parent
|
|
330
|
+
data = _fail_if_bad(cli.post("/v2/folders", json=body), "mkdir")
|
|
331
|
+
f = data["folder"]
|
|
332
|
+
console.print(f"[green]ok[/] folder #{f['id']} · {f['name']}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.command()
|
|
336
|
+
def share(
|
|
337
|
+
uid: str = typer.Argument(...),
|
|
338
|
+
role: str = typer.Option("viewer", "--role", "-r", help="viewer | commenter | editor"),
|
|
339
|
+
expires: Optional[str] = typer.Option(None, "--expires", help="ISO 8601 expiry"),
|
|
340
|
+
password: Optional[str] = typer.Option(None, "--password"),
|
|
341
|
+
max_uses: Optional[int] = typer.Option(None, "--max-uses"),
|
|
342
|
+
):
|
|
343
|
+
"""Create a public share link."""
|
|
344
|
+
with _client() as cli:
|
|
345
|
+
body: dict = {"role": role}
|
|
346
|
+
if expires:
|
|
347
|
+
body["expires_at"] = expires
|
|
348
|
+
if password:
|
|
349
|
+
body["password"] = password
|
|
350
|
+
if max_uses:
|
|
351
|
+
body["max_uses"] = max_uses
|
|
352
|
+
data = _fail_if_bad(cli.post(f"/v2/files/{uid}/share", json=body), "share")
|
|
353
|
+
cfg = _load()
|
|
354
|
+
base = cfg.get("base_url", DEFAULT_BASE)
|
|
355
|
+
url = f"{base}/v2/ui/s/{data['share']['token']}"
|
|
356
|
+
console.print(f"[green]share[/] {url}")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
tokens_app = typer.Typer(no_args_is_help=True, help="Manage API tokens")
|
|
360
|
+
app.add_typer(tokens_app, name="tokens")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@tokens_app.command("list")
|
|
364
|
+
def tokens_list():
|
|
365
|
+
with _client() as cli:
|
|
366
|
+
data = _fail_if_bad(cli.get("/v2/tokens"), "tokens")
|
|
367
|
+
t = Table(header_style="bold")
|
|
368
|
+
t.add_column("Name")
|
|
369
|
+
t.add_column("Prefix", style="cyan")
|
|
370
|
+
t.add_column("Scopes", style="yellow")
|
|
371
|
+
t.add_column("Last used", style="dim")
|
|
372
|
+
for row in data.get("tokens", []):
|
|
373
|
+
scopes = ",".join(row.get("scopes") or []) or "default"
|
|
374
|
+
t.add_row(
|
|
375
|
+
row["name"], row["prefix"] + "…",
|
|
376
|
+
scopes, row.get("last_used_at") or "never",
|
|
377
|
+
)
|
|
378
|
+
console.print(t)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@tokens_app.command("create")
|
|
382
|
+
def tokens_create(
|
|
383
|
+
name: str = typer.Argument(...),
|
|
384
|
+
scopes: Optional[str] = typer.Option(None, "--scopes", "-s", help="comma-separated scopes"),
|
|
385
|
+
):
|
|
386
|
+
sc = [x.strip() for x in (scopes or "").split(",") if x.strip()]
|
|
387
|
+
with _client() as cli:
|
|
388
|
+
data = _fail_if_bad(cli.post("/v2/tokens", json={"name": name, "scopes": sc}), "create token")
|
|
389
|
+
console.print(f"[green]ok[/] {data['token']['name']}")
|
|
390
|
+
console.print(f"[bold yellow]token (shown once):[/] {data['token']['token']}")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@tokens_app.command("revoke")
|
|
394
|
+
def tokens_revoke(id: int = typer.Argument(...)):
|
|
395
|
+
with _client() as cli:
|
|
396
|
+
_fail_if_bad(cli.delete(f"/v2/tokens/{id}"), "revoke")
|
|
397
|
+
console.print(f"[green]ok[/] revoked #{id}")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@app.command(name="migrate-self")
|
|
401
|
+
def migrate_self(
|
|
402
|
+
legacy_token: str = typer.Argument(...),
|
|
403
|
+
base_url: str = typer.Option(DEFAULT_BASE, "--base-url", "-u"),
|
|
404
|
+
):
|
|
405
|
+
"""Migrate a legacy account into v2 (no auth required)."""
|
|
406
|
+
with httpx.Client(base_url=base_url, timeout=60) as cli:
|
|
407
|
+
r = cli.post("/v2/migrate/legacy/self", json={"legacy_token": legacy_token})
|
|
408
|
+
data = _fail_if_bad(r, "migrate")
|
|
409
|
+
console.print(
|
|
410
|
+
f"[green]ok[/] migrated={data.get('migrated_count', 0)} "
|
|
411
|
+
f"already={data.get('already')} stub={data.get('stub')} "
|
|
412
|
+
f"cloud2_user_id={data.get('cloud2_user_id')}"
|
|
413
|
+
)
|
|
414
|
+
console.print(f"login: {base_url}{data['login_url']}")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def main():
|
|
418
|
+
app()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if __name__ == "__main__":
|
|
422
|
+
main()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloud-tc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal client for OnlySq Cloud 2.0 — talks to https://cloud.onlysq.ru via REST
|
|
5
|
+
Author-email: OnlySq <contact@onlysq.ru>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/xOnlySq/cloud-tc
|
|
8
|
+
Project-URL: Repository, https://github.com/xOnlySq/cloud-tc
|
|
9
|
+
Project-URL: Issues, https://github.com/xOnlySq/cloud-tc/issues
|
|
10
|
+
Project-URL: Service, https://cloud.onlysq.ru
|
|
11
|
+
Project-URL: Documentation, https://cloud.onlysq.ru/docs
|
|
12
|
+
Keywords: onlysq,cloud,telegram,cli,storage,drive
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Operating System :: MacOS
|
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: System :: Archiving
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: httpx>=0.27
|
|
33
|
+
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: rich>=13.7
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# cloud-tc
|
|
38
|
+
|
|
39
|
+
Terminal client for **OnlySq Cloud 2.0** (`https://cloud.onlysq.ru`). Talks to the public v2 REST API — no Telegram, no Postgres on your machine, just `httpx` + `typer` + `rich`.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
From PyPI (when published):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install cloud-tc
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Straight from git:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install git+https://github.com/xOnlySq/cloud-tc.git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or for a local checkout (with `-e` for editable):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/xOnlySq/cloud-tc.git
|
|
59
|
+
cd cloud-tc
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both `tc` and `cloud-tc` console scripts are installed.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
1. Create an API token in the web UI: `https://cloud.onlysq.ru/v2/ui/settings` → "new token".
|
|
68
|
+
2. Log in locally:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
tc login --token tck_xxxxxxxxxxxxxxxxxxxx
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The token is stored in `~/.cloud-tc/config.json` (0600).
|
|
75
|
+
|
|
76
|
+
3. Use it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
tc me
|
|
80
|
+
tc quota
|
|
81
|
+
tc ls
|
|
82
|
+
tc upload ./photo.jpg
|
|
83
|
+
tc share <uid> --role viewer
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Commands
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `tc login --token tck_…` | Save and verify an API token |
|
|
91
|
+
| `tc logout` | Remove local config |
|
|
92
|
+
| `tc info` | Show local config |
|
|
93
|
+
| `tc version` | Print package version |
|
|
94
|
+
| `tc me` | Show current user profile |
|
|
95
|
+
| `tc quota` | Show used / total quota |
|
|
96
|
+
| `tc ls [FOLDER_ID]` | List files |
|
|
97
|
+
| `tc upload PATH [-f FOLDER]` | Upload a local file |
|
|
98
|
+
| `tc download UID [-o PATH]` | Download a file |
|
|
99
|
+
| `tc rm UID` | Move to trash |
|
|
100
|
+
| `tc restore UID` | Restore from trash |
|
|
101
|
+
| `tc mv UID FOLDER_ID` | Move file to another folder |
|
|
102
|
+
| `tc rename UID NEW_NAME` | Rename |
|
|
103
|
+
| `tc mkdir NAME [-p PARENT_ID]` | Create folder |
|
|
104
|
+
| `tc share UID [-r ROLE] [--password] [--expires] [--max-uses]` | Create a share link |
|
|
105
|
+
| `tc tokens list / create NAME / revoke ID` | Manage API tokens |
|
|
106
|
+
| `tc migrate-self LEGACY_TOKEN` | Move a legacy account to v2 (no auth required) |
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
You can override the server with environment variables (useful for self-hosted instances):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export TC_BASE_URL="https://cloud.example.com"
|
|
114
|
+
export TC_TOKEN="tck_..."
|
|
115
|
+
tc ls
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`TC_TOKEN` overrides the saved token if set. `TC_BASE_URL` overrides the configured base URL.
|
|
119
|
+
|
|
120
|
+
Config file: `~/.cloud-tc/config.json` — just `{ "token": "...", "base_url": "..." }`.
|
|
121
|
+
|
|
122
|
+
## Examples
|
|
123
|
+
|
|
124
|
+
Upload everything from a directory into a folder:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
for f in *.jpg; do tc upload "$f" -f 42; done
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Pipe a file through `tc upload` (Linux):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
tc upload /dev/stdin <<< "hello" # named pipe trick, see issues for binary
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Quick public link for a file:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
UID=$(tc upload ./report.pdf | awk '{print $2}')
|
|
140
|
+
tc share "$UID" --role viewer
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Batch-migrate one legacy token from any script:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
tc migrate-self USERS_LEGACY_TOKEN
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT. See [LICENSE](LICENSE).
|
|
152
|
+
|
|
153
|
+
## Related
|
|
154
|
+
|
|
155
|
+
- Cloud UI: <https://cloud.onlysq.ru>
|
|
156
|
+
- REST API docs: <https://cloud.onlysq.ru/docs> (see "Cloud 2.0" group)
|
|
157
|
+
- Service / issues: <https://github.com/xOnlySq/cloud-tc/issues>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
cloud_tc/__init__.py
|
|
5
|
+
cloud_tc/cli.py
|
|
6
|
+
cloud_tc.egg-info/PKG-INFO
|
|
7
|
+
cloud_tc.egg-info/SOURCES.txt
|
|
8
|
+
cloud_tc.egg-info/dependency_links.txt
|
|
9
|
+
cloud_tc.egg-info/entry_points.txt
|
|
10
|
+
cloud_tc.egg-info/requires.txt
|
|
11
|
+
cloud_tc.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cloud_tc
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cloud-tc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Terminal client for OnlySq Cloud 2.0 — talks to https://cloud.onlysq.ru via REST"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "OnlySq", email = "contact@onlysq.ru" }]
|
|
14
|
+
keywords = ["onlysq", "cloud", "telegram", "cli", "storage", "drive"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Operating System :: Microsoft :: Windows",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
30
|
+
"Topic :: System :: Archiving",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"httpx>=0.27",
|
|
35
|
+
"typer>=0.12",
|
|
36
|
+
"rich>=13.7",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/xOnlySq/cloud-tc"
|
|
41
|
+
Repository = "https://github.com/xOnlySq/cloud-tc"
|
|
42
|
+
Issues = "https://github.com/xOnlySq/cloud-tc/issues"
|
|
43
|
+
Service = "https://cloud.onlysq.ru"
|
|
44
|
+
Documentation = "https://cloud.onlysq.ru/docs"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
tc = "cloud_tc.cli:app"
|
|
48
|
+
cloud-tc = "cloud_tc.cli:app"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
include = ["cloud_tc*"]
|
cloud_tc-0.1.0/setup.cfg
ADDED