supernote 0.5.0__tar.gz → 0.6.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.
- {supernote-0.5.0 → supernote-0.6.0}/PKG-INFO +9 -5
- {supernote-0.5.0 → supernote-0.6.0}/README.md +8 -4
- {supernote-0.5.0 → supernote-0.6.0}/pyproject.toml +1 -1
- supernote-0.6.0/supernote/cli/server.py +137 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/app.py +1 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/config.py +2 -2
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/file.py +11 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/file.py +99 -33
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/file.py +76 -98
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/storage.py +101 -132
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/user.py +15 -6
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/PKG-INFO +9 -5
- supernote-0.5.0/supernote/cli/server.py +0 -100
- {supernote-0.5.0 → supernote-0.6.0}/LICENSE +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/setup.cfg +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/client.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/main.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/notebook.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/api_model.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/auth.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/client.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/cloud_client.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/exceptions.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/client/login_client.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/color.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/converter.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/decoder.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/exceptions.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/fileformat.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/manipulator.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/parser.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/utils.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/py.typed +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/auth.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/base.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/auth.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/decorators.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/system.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/__init__.py +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/SOURCES.txt +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/dependency_links.txt +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/entry_points.txt +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/requires.txt +0 -0
- {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supernote
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services
|
|
5
5
|
Author-email: jya <jya@wizmy.net>, Allen Porter <allen.porter@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -82,8 +82,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
|
|
|
82
82
|
### Run Private Server
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
#
|
|
86
|
-
supernote-server
|
|
85
|
+
# Generate configuration
|
|
86
|
+
supernote-server config init > config.yaml
|
|
87
|
+
|
|
88
|
+
# Generate users (outputs YAML, append to users.yaml)
|
|
89
|
+
supernote-server user add alice >> users.yaml
|
|
87
90
|
|
|
88
91
|
# Start server
|
|
89
92
|
supernote-server serve
|
|
@@ -95,9 +98,10 @@ supernote-server serve
|
|
|
95
98
|
# Build image
|
|
96
99
|
docker build -t supernote-server .
|
|
97
100
|
|
|
98
|
-
#
|
|
101
|
+
# Generate initial configuration
|
|
99
102
|
mkdir config
|
|
100
|
-
docker run --rm
|
|
103
|
+
docker run --rm supernote-server supernote-server config init > config/config.yaml
|
|
104
|
+
docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
|
|
101
105
|
|
|
102
106
|
# Run server
|
|
103
107
|
docker run -d -p 8080:8080 \
|
|
@@ -52,8 +52,11 @@ limitation. All credit goes to the original authors of [supernote-tool](https://
|
|
|
52
52
|
### Run Private Server
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
#
|
|
56
|
-
supernote-server
|
|
55
|
+
# Generate configuration
|
|
56
|
+
supernote-server config init > config.yaml
|
|
57
|
+
|
|
58
|
+
# Generate users (outputs YAML, append to users.yaml)
|
|
59
|
+
supernote-server user add alice >> users.yaml
|
|
57
60
|
|
|
58
61
|
# Start server
|
|
59
62
|
supernote-server serve
|
|
@@ -65,9 +68,10 @@ supernote-server serve
|
|
|
65
68
|
# Build image
|
|
66
69
|
docker build -t supernote-server .
|
|
67
70
|
|
|
68
|
-
#
|
|
71
|
+
# Generate initial configuration
|
|
69
72
|
mkdir config
|
|
70
|
-
docker run --rm
|
|
73
|
+
docker run --rm supernote-server supernote-server config init > config/config.yaml
|
|
74
|
+
docker run --rm -it supernote-server supernote-server user add alice >> config/users.yaml
|
|
71
75
|
|
|
72
76
|
# Run server
|
|
73
77
|
docker run -d -p 8080:8080 \
|
|
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "supernote"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
license-files = ["LICENSE"]
|
|
10
10
|
description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Server CLI commands."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import secrets
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from supernote.server import app as server_app
|
|
10
|
+
from supernote.server.config import AuthConfig, ServerConfig, UsersConfig
|
|
11
|
+
from supernote.server.services.user import UserService
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_auth_config(config_dir: str | None) -> AuthConfig:
|
|
15
|
+
# Load base config
|
|
16
|
+
server_config = ServerConfig.load(config_dir)
|
|
17
|
+
return server_config.auth
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_users(config_dir: str | None):
|
|
21
|
+
config = get_auth_config(config_dir)
|
|
22
|
+
service = UserService(config)
|
|
23
|
+
for user in service.list_users():
|
|
24
|
+
status = "active" if user.is_active else "inactive"
|
|
25
|
+
print(f"{user.username} ({status})")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_user(username: str, password: str | None = None):
|
|
29
|
+
if password is None:
|
|
30
|
+
password = getpass.getpass(f"Password for {username}: ")
|
|
31
|
+
|
|
32
|
+
# Generate the YAML snippet for a single user
|
|
33
|
+
user_entry = UserService.create_user_entry(username, password)
|
|
34
|
+
users_config = UsersConfig(users=[user_entry])
|
|
35
|
+
|
|
36
|
+
# Print YAML to stdout
|
|
37
|
+
print("# Generated Supernote User Entry")
|
|
38
|
+
print("# Append this to your users.yaml file.")
|
|
39
|
+
print(yaml.safe_dump(users_config.to_dict(), default_flow_style=False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def config_init():
|
|
43
|
+
"""Output a default config.yaml with a generated secret."""
|
|
44
|
+
secret = secrets.token_hex(32)
|
|
45
|
+
config = ServerConfig()
|
|
46
|
+
config.auth.secret_key = secret
|
|
47
|
+
|
|
48
|
+
print("# Generated Supernote Server Configuration")
|
|
49
|
+
print("# Save this as config.yaml.")
|
|
50
|
+
print(yaml.safe_dump(config.to_dict(), default_flow_style=False))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_secret():
|
|
54
|
+
"""Output a 32-byte random hex secret."""
|
|
55
|
+
print(secrets.token_hex(32))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def add_parser(subparsers):
|
|
59
|
+
# Common parent parser for server-related commands that need config access
|
|
60
|
+
base_parser = argparse.ArgumentParser(add_help=False)
|
|
61
|
+
base_parser.add_argument(
|
|
62
|
+
"--config-dir",
|
|
63
|
+
type=str,
|
|
64
|
+
default=None,
|
|
65
|
+
help="Path to configuration directory (default: config/)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 'serve' subcommand
|
|
69
|
+
parser_serve = subparsers.add_parser(
|
|
70
|
+
"serve",
|
|
71
|
+
parents=[base_parser],
|
|
72
|
+
help="Start the Supernote Private Cloud server",
|
|
73
|
+
)
|
|
74
|
+
parser_serve.set_defaults(func=server_app.run)
|
|
75
|
+
|
|
76
|
+
# User management
|
|
77
|
+
parser_user = subparsers.add_parser("user", help="User management commands")
|
|
78
|
+
user_subparsers = parser_user.add_subparsers(dest="user_command")
|
|
79
|
+
|
|
80
|
+
# user list
|
|
81
|
+
parser_user_list = user_subparsers.add_parser(
|
|
82
|
+
"list",
|
|
83
|
+
parents=[base_parser],
|
|
84
|
+
help="List all users in the current config file",
|
|
85
|
+
)
|
|
86
|
+
parser_user_list.set_defaults(
|
|
87
|
+
func=lambda args: list_users(getattr(args, "config_dir", None))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# user add
|
|
91
|
+
parser_user_add = user_subparsers.add_parser(
|
|
92
|
+
"add",
|
|
93
|
+
parents=[base_parser],
|
|
94
|
+
help="Generate a user configuration snippet",
|
|
95
|
+
)
|
|
96
|
+
parser_user_add.add_argument("username", type=str, help="Username for the new user")
|
|
97
|
+
parser_user_add.add_argument(
|
|
98
|
+
"--password", type=str, help="Password (if omitted, prompt interactively)"
|
|
99
|
+
)
|
|
100
|
+
parser_user_add.set_defaults(
|
|
101
|
+
func=lambda args: add_user(args.username, args.password)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# config group
|
|
105
|
+
parser_config = subparsers.add_parser("config", help="Configuration utilities")
|
|
106
|
+
config_subparsers = parser_config.add_subparsers(dest="config_command")
|
|
107
|
+
|
|
108
|
+
# config init
|
|
109
|
+
parser_config_init = config_subparsers.add_parser(
|
|
110
|
+
"init",
|
|
111
|
+
parents=[base_parser],
|
|
112
|
+
help="Generate a default config.yaml",
|
|
113
|
+
)
|
|
114
|
+
parser_config_init.set_defaults(func=lambda _: config_init())
|
|
115
|
+
|
|
116
|
+
# config secret
|
|
117
|
+
parser_config_secret = config_subparsers.add_parser(
|
|
118
|
+
"generate-secret",
|
|
119
|
+
parents=[base_parser],
|
|
120
|
+
help="Generate a secure random secret",
|
|
121
|
+
)
|
|
122
|
+
parser_config_secret.set_defaults(func=lambda _: generate_secret())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
parser = argparse.ArgumentParser(description="Supernote Server CLI")
|
|
127
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
128
|
+
add_parser(subparsers)
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
if hasattr(args, "func"):
|
|
131
|
+
args.func(args)
|
|
132
|
+
else:
|
|
133
|
+
parser.print_help()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
|
@@ -123,6 +123,7 @@ def create_app(config: ServerConfig | None = None) -> web.Application:
|
|
|
123
123
|
app["storage_service"] = storage_service
|
|
124
124
|
app["user_service"] = UserService(config.auth)
|
|
125
125
|
app["file_service"] = FileService(storage_service)
|
|
126
|
+
app["sync_locks"] = {} # user -> (equipment_no, expiry_time)
|
|
126
127
|
|
|
127
128
|
# Register routes
|
|
128
129
|
app.add_routes(system.routes)
|
|
@@ -92,9 +92,9 @@ class ServerConfig(DataClassDictMixin):
|
|
|
92
92
|
)
|
|
93
93
|
config = cls()
|
|
94
94
|
|
|
95
|
-
# Resolve users_file relative to config_dir if not
|
|
95
|
+
# Resolve users_file relative to config_dir if it's just a filename (not already a path)
|
|
96
96
|
users_path = Path(config.auth.users_file)
|
|
97
|
-
if not users_path.is_absolute():
|
|
97
|
+
if not users_path.is_absolute() and len(users_path.parts) == 1:
|
|
98
98
|
config.auth.users_file = str(config_dir_path / users_path)
|
|
99
99
|
|
|
100
100
|
# 4. Generate secret if missing AND not provided by env var
|
|
@@ -25,6 +25,17 @@ class SyncStartRequest(DataClassJSONMixin):
|
|
|
25
25
|
code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@dataclass
|
|
29
|
+
class SyncEndRequest(DataClassJSONMixin):
|
|
30
|
+
equipment_no: str = field(metadata=field_options(alias="equipmentNo"))
|
|
31
|
+
flag: str | None = None # "true" or "false"
|
|
32
|
+
|
|
33
|
+
class Config(BaseConfig):
|
|
34
|
+
serialize_by_alias = True
|
|
35
|
+
omit_none = True
|
|
36
|
+
code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG] # type: ignore[list-item]
|
|
37
|
+
|
|
38
|
+
|
|
28
39
|
@dataclass
|
|
29
40
|
class ListFolderRequest(DataClassJSONMixin):
|
|
30
41
|
path: str = "/"
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
import time
|
|
3
4
|
import urllib.parse
|
|
4
5
|
|
|
5
|
-
from aiohttp import web
|
|
6
|
+
from aiohttp import BodyPartReader, web
|
|
6
7
|
|
|
7
|
-
from ..models.base import BaseResponse
|
|
8
|
+
from ..models.base import BaseResponse, create_error_response
|
|
8
9
|
from ..models.file import (
|
|
9
10
|
AllocationVO,
|
|
10
11
|
CapacityResponse,
|
|
@@ -23,6 +24,7 @@ from ..models.file import (
|
|
|
23
24
|
ListFolderResponse,
|
|
24
25
|
RecycleFileListRequest,
|
|
25
26
|
RecycleFileRequest,
|
|
27
|
+
SyncEndRequest,
|
|
26
28
|
SyncStartRequest,
|
|
27
29
|
SyncStartResponse,
|
|
28
30
|
UploadApplyRequest,
|
|
@@ -35,16 +37,43 @@ logger = logging.getLogger(__name__)
|
|
|
35
37
|
routes = web.RouteTableDef()
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
SYNC_LOCK_TIMEOUT = 300 # 5 minutes
|
|
41
|
+
|
|
42
|
+
|
|
38
43
|
@routes.post("/api/file/2/files/synchronous/start")
|
|
39
44
|
async def handle_sync_start(request: web.Request) -> web.Response:
|
|
40
45
|
# Endpoint: POST /api/file/2/files/synchronous/start
|
|
41
46
|
# Purpose: Start a file synchronization session.
|
|
42
47
|
# Response: SynchronousStartLocalVO
|
|
43
48
|
req_data = SyncStartRequest.from_dict(await request.json())
|
|
49
|
+
user_email = request["user"]
|
|
50
|
+
sync_locks = request.app["sync_locks"]
|
|
51
|
+
storage_service: StorageService = request.app["storage_service"]
|
|
52
|
+
|
|
53
|
+
loop = asyncio.get_running_loop()
|
|
54
|
+
is_empty = await loop.run_in_executor(None, storage_service.is_empty, user_email)
|
|
55
|
+
|
|
56
|
+
now = time.time()
|
|
57
|
+
if user_email in sync_locks:
|
|
58
|
+
owner_eq, expiry = sync_locks[user_email]
|
|
59
|
+
if now < expiry and owner_eq != req_data.equipment_no:
|
|
60
|
+
logger.info(
|
|
61
|
+
f"Sync conflict: user {user_email} already syncing from {owner_eq}"
|
|
62
|
+
)
|
|
63
|
+
return web.json_response(
|
|
64
|
+
create_error_response(
|
|
65
|
+
error_msg="Another device is synchronizing",
|
|
66
|
+
error_code="E0078",
|
|
67
|
+
).to_dict(),
|
|
68
|
+
status=409,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
sync_locks[user_email] = (req_data.equipment_no, now + SYNC_LOCK_TIMEOUT)
|
|
72
|
+
|
|
44
73
|
return web.json_response(
|
|
45
74
|
SyncStartResponse(
|
|
46
75
|
equipment_no=req_data.equipment_no,
|
|
47
|
-
syn_type=
|
|
76
|
+
syn_type=not is_empty,
|
|
48
77
|
).to_dict()
|
|
49
78
|
)
|
|
50
79
|
|
|
@@ -53,8 +82,17 @@ async def handle_sync_start(request: web.Request) -> web.Response:
|
|
|
53
82
|
async def handle_sync_end(request: web.Request) -> web.Response:
|
|
54
83
|
# Endpoint: POST /api/file/2/files/synchronous/end
|
|
55
84
|
# Purpose: End a file synchronization session.
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
req_data = SyncEndRequest.from_dict(await request.json())
|
|
86
|
+
user_email = request["user"]
|
|
87
|
+
|
|
88
|
+
# Release lock
|
|
89
|
+
sync_locks = request.app["sync_locks"]
|
|
90
|
+
if user_email in sync_locks:
|
|
91
|
+
owner_eq, _ = sync_locks[user_email]
|
|
92
|
+
if owner_eq == req_data.equipment_no:
|
|
93
|
+
del sync_locks[user_email]
|
|
94
|
+
|
|
95
|
+
return web.json_response(BaseResponse(success=True).to_dict())
|
|
58
96
|
|
|
59
97
|
|
|
60
98
|
@routes.post("/api/file/2/files/list_folder")
|
|
@@ -66,10 +104,13 @@ async def handle_list_folder(request: web.Request) -> web.Response:
|
|
|
66
104
|
|
|
67
105
|
req_data = ListFolderRequest.from_dict(await request.json())
|
|
68
106
|
path_str = req_data.path
|
|
107
|
+
user_email = request["user"]
|
|
69
108
|
file_service: FileService = request.app["file_service"]
|
|
70
109
|
|
|
71
110
|
loop = asyncio.get_running_loop()
|
|
72
|
-
entries = await loop.run_in_executor(
|
|
111
|
+
entries = await loop.run_in_executor(
|
|
112
|
+
None, file_service.list_folder, user_email, path_str
|
|
113
|
+
)
|
|
73
114
|
|
|
74
115
|
return web.json_response(
|
|
75
116
|
ListFolderResponse(
|
|
@@ -86,10 +127,13 @@ async def handle_capacity_query(request: web.Request) -> web.Response:
|
|
|
86
127
|
|
|
87
128
|
req_data = await request.json()
|
|
88
129
|
equipment_no = req_data.get("equipmentNo", "")
|
|
89
|
-
|
|
130
|
+
user_email = request["user"]
|
|
131
|
+
|
|
90
132
|
storage_service: StorageService = request.app["storage_service"]
|
|
91
133
|
loop = asyncio.get_running_loop()
|
|
92
|
-
used = await loop.run_in_executor(
|
|
134
|
+
used = await loop.run_in_executor(
|
|
135
|
+
None, storage_service.get_storage_usage, user_email
|
|
136
|
+
)
|
|
93
137
|
|
|
94
138
|
return web.json_response(
|
|
95
139
|
CapacityResponse(
|
|
@@ -111,10 +155,13 @@ async def handle_query_by_path(request: web.Request) -> web.Response:
|
|
|
111
155
|
|
|
112
156
|
req_data = FileQueryRequest.from_dict(await request.json())
|
|
113
157
|
path_str = req_data.path
|
|
158
|
+
user_email = request["user"]
|
|
114
159
|
file_service: FileService = request.app["file_service"]
|
|
115
160
|
|
|
116
161
|
loop = asyncio.get_running_loop()
|
|
117
|
-
entries_vo = await loop.run_in_executor(
|
|
162
|
+
entries_vo = await loop.run_in_executor(
|
|
163
|
+
None, file_service.get_file_info, user_email, path_str
|
|
164
|
+
)
|
|
118
165
|
|
|
119
166
|
return web.json_response(
|
|
120
167
|
FileQueryResponse(
|
|
@@ -131,10 +178,13 @@ async def handle_query_v3(request: web.Request) -> web.Response:
|
|
|
131
178
|
|
|
132
179
|
req_data = FileQueryByIdRequest.from_dict(await request.json())
|
|
133
180
|
file_id = req_data.id
|
|
181
|
+
user_email = request["user"]
|
|
134
182
|
file_service: FileService = request.app["file_service"]
|
|
135
183
|
|
|
136
184
|
loop = asyncio.get_running_loop()
|
|
137
|
-
entries_vo = await loop.run_in_executor(
|
|
185
|
+
entries_vo = await loop.run_in_executor(
|
|
186
|
+
None, file_service.get_file_info, user_email, file_id
|
|
187
|
+
)
|
|
138
188
|
|
|
139
189
|
return web.json_response(
|
|
140
190
|
FileQueryResponse(
|
|
@@ -152,10 +202,11 @@ async def handle_upload_apply(request: web.Request) -> web.Response:
|
|
|
152
202
|
|
|
153
203
|
req_data = UploadApplyRequest.from_dict(await request.json())
|
|
154
204
|
file_name = req_data.file_name
|
|
205
|
+
user_email = request["user"]
|
|
155
206
|
file_service: FileService = request.app["file_service"]
|
|
156
207
|
|
|
157
208
|
response = file_service.apply_upload(
|
|
158
|
-
file_name, req_data.equipment_no or "", request.host
|
|
209
|
+
user_email, file_name, req_data.equipment_no or "", request.host
|
|
159
210
|
)
|
|
160
211
|
|
|
161
212
|
return web.json_response(response.to_dict())
|
|
@@ -168,32 +219,26 @@ async def handle_upload_data(request: web.Request) -> web.Response:
|
|
|
168
219
|
# Purpose: Receive the actual file content.
|
|
169
220
|
|
|
170
221
|
filename = request.match_info["filename"]
|
|
222
|
+
user_email = request["user"]
|
|
171
223
|
storage_service: StorageService = request.app["storage_service"]
|
|
172
224
|
|
|
173
225
|
# The device sends multipart/form-data
|
|
174
|
-
# Note: trace_middleware might have consumed the body already if we are not careful.
|
|
175
|
-
# But trace_middleware uses request.read() which caches the body, so it should be fine?
|
|
176
|
-
# Actually, request.read() reads the whole body into memory.
|
|
177
|
-
# request.multipart() expects to read from the stream.
|
|
178
|
-
# If the body is already read, we might need to handle it differently.
|
|
179
|
-
|
|
180
226
|
if request._read_bytes:
|
|
181
227
|
# Body already read by middleware
|
|
182
|
-
# We need to reconstruct a multipart reader or just parse it manually if possible.
|
|
183
|
-
# However, aiohttp's multipart reader expects a stream.
|
|
184
|
-
# Since we are in a "lite" server, maybe we can just skip the middleware for this route
|
|
185
|
-
# or make the middleware smarter.
|
|
186
|
-
# For now, let's try to use the standard multipart reader which might fail if stream is consumed.
|
|
187
228
|
pass
|
|
188
229
|
|
|
189
230
|
reader = await request.multipart()
|
|
190
231
|
|
|
191
232
|
# Read the first part (which should be the file)
|
|
192
233
|
field = await reader.next()
|
|
193
|
-
if field.name == "file":
|
|
234
|
+
if isinstance(field, BodyPartReader) and field.name == "file":
|
|
194
235
|
# Write to temp file using non-blocking I/O
|
|
195
|
-
total_bytes = await storage_service.save_temp_file(
|
|
196
|
-
|
|
236
|
+
total_bytes = await storage_service.save_temp_file(
|
|
237
|
+
user_email, filename, field.read_chunk
|
|
238
|
+
)
|
|
239
|
+
logger.info(
|
|
240
|
+
f"Received upload for {filename} (user: {user_email}): {total_bytes} bytes"
|
|
241
|
+
)
|
|
197
242
|
|
|
198
243
|
return web.Response(status=200)
|
|
199
244
|
|
|
@@ -205,6 +250,7 @@ async def handle_upload_finish(request: web.Request) -> web.Response:
|
|
|
205
250
|
# Response: FileUploadFinishLocalVO
|
|
206
251
|
|
|
207
252
|
req_data = UploadFinishRequest.from_dict(await request.json())
|
|
253
|
+
user_email = request["user"]
|
|
208
254
|
file_service: FileService = request.app["file_service"]
|
|
209
255
|
|
|
210
256
|
loop = asyncio.get_running_loop()
|
|
@@ -213,6 +259,7 @@ async def handle_upload_finish(request: web.Request) -> web.Response:
|
|
|
213
259
|
response = await loop.run_in_executor(
|
|
214
260
|
None,
|
|
215
261
|
file_service.finish_upload,
|
|
262
|
+
user_email,
|
|
216
263
|
req_data.file_name,
|
|
217
264
|
req_data.path,
|
|
218
265
|
req_data.content_hash,
|
|
@@ -234,10 +281,11 @@ async def handle_download_apply(request: web.Request) -> web.Response:
|
|
|
234
281
|
|
|
235
282
|
req_data = DownloadApplyRequest.from_dict(await request.json())
|
|
236
283
|
file_id = req_data.id # This is the relative path now
|
|
284
|
+
user_email = request["user"]
|
|
237
285
|
storage_service: StorageService = request.app["storage_service"]
|
|
238
286
|
|
|
239
287
|
# Verify file exists
|
|
240
|
-
target_path = storage_service.resolve_path(file_id)
|
|
288
|
+
target_path = storage_service.resolve_path(user_email, file_id)
|
|
241
289
|
if not target_path.exists():
|
|
242
290
|
return web.json_response(
|
|
243
291
|
BaseResponse(success=False, error_msg="File not found").to_dict(),
|
|
@@ -260,11 +308,12 @@ async def handle_download_data(request: web.Request) -> web.StreamResponse:
|
|
|
260
308
|
if not path_str:
|
|
261
309
|
return web.Response(status=400, text="Missing path")
|
|
262
310
|
|
|
311
|
+
user_email = request["user"]
|
|
263
312
|
storage_service: StorageService = request.app["storage_service"]
|
|
264
|
-
target_path = storage_service.resolve_path(path_str)
|
|
313
|
+
target_path = storage_service.resolve_path(user_email, path_str)
|
|
265
314
|
|
|
266
315
|
# Security check: prevent directory traversal
|
|
267
|
-
if not storage_service.is_safe_path(target_path):
|
|
316
|
+
if not storage_service.is_safe_path(user_email, target_path):
|
|
268
317
|
return web.Response(status=403, text="Access denied")
|
|
269
318
|
|
|
270
319
|
if not target_path.exists():
|
|
@@ -279,9 +328,11 @@ async def handle_create_folder(request: web.Request) -> web.Response:
|
|
|
279
328
|
# Purpose: Create a new folder.
|
|
280
329
|
|
|
281
330
|
req_data = CreateDirectoryRequest.from_dict(await request.json())
|
|
331
|
+
user_email = request["user"]
|
|
282
332
|
file_service: FileService = request.app["file_service"]
|
|
283
333
|
|
|
284
334
|
response = file_service.create_directory(
|
|
335
|
+
user_email,
|
|
285
336
|
req_data.path,
|
|
286
337
|
req_data.equipment_no,
|
|
287
338
|
)
|
|
@@ -295,10 +346,12 @@ async def handle_delete_folder(request: web.Request) -> web.Response:
|
|
|
295
346
|
# Purpose: Delete a file or folder.
|
|
296
347
|
|
|
297
348
|
req_data = DeleteRequest.from_dict(await request.json())
|
|
349
|
+
user_email = request["user"]
|
|
298
350
|
file_service: FileService = request.app["file_service"]
|
|
299
351
|
|
|
300
352
|
# Request has 'id' (int) now
|
|
301
353
|
response = file_service.delete_item(
|
|
354
|
+
user_email,
|
|
302
355
|
req_data.id,
|
|
303
356
|
req_data.equipment_no,
|
|
304
357
|
)
|
|
@@ -312,9 +365,11 @@ async def handle_move_file(request: web.Request) -> web.Response:
|
|
|
312
365
|
# Purpose: Move a file or folder.
|
|
313
366
|
|
|
314
367
|
req_data = FileMoveRequest.from_dict(await request.json())
|
|
368
|
+
user_email = request["user"]
|
|
315
369
|
file_service: FileService = request.app["file_service"]
|
|
316
370
|
|
|
317
371
|
response = file_service.move_item(
|
|
372
|
+
user_email,
|
|
318
373
|
req_data.id,
|
|
319
374
|
req_data.to_path,
|
|
320
375
|
req_data.autorename,
|
|
@@ -330,9 +385,11 @@ async def handle_copy_file(request: web.Request) -> web.Response:
|
|
|
330
385
|
# Purpose: Copy a file or folder.
|
|
331
386
|
|
|
332
387
|
req_data = FileCopyRequest.from_dict(await request.json())
|
|
388
|
+
user_email = request["user"]
|
|
333
389
|
file_service: FileService = request.app["file_service"]
|
|
334
390
|
|
|
335
391
|
response = file_service.copy_item(
|
|
392
|
+
user_email,
|
|
336
393
|
req_data.id,
|
|
337
394
|
req_data.to_path,
|
|
338
395
|
req_data.autorename,
|
|
@@ -348,10 +405,15 @@ async def handle_recycle_list(request: web.Request) -> web.Response:
|
|
|
348
405
|
# Purpose: List files in recycle bin.
|
|
349
406
|
|
|
350
407
|
req_data = RecycleFileListRequest.from_dict(await request.json())
|
|
408
|
+
user_email = request["user"]
|
|
351
409
|
file_service: FileService = request.app["file_service"]
|
|
352
410
|
|
|
353
411
|
response = file_service.list_recycle(
|
|
354
|
-
|
|
412
|
+
user_email,
|
|
413
|
+
req_data.order,
|
|
414
|
+
req_data.sequence,
|
|
415
|
+
req_data.page_no,
|
|
416
|
+
req_data.page_size,
|
|
355
417
|
)
|
|
356
418
|
|
|
357
419
|
return web.json_response(response.to_dict())
|
|
@@ -363,9 +425,10 @@ async def handle_recycle_delete(request: web.Request) -> web.Response:
|
|
|
363
425
|
# Purpose: Permanently delete items from recycle bin.
|
|
364
426
|
|
|
365
427
|
req_data = RecycleFileRequest.from_dict(await request.json())
|
|
428
|
+
user_email = request["user"]
|
|
366
429
|
file_service: FileService = request.app["file_service"]
|
|
367
430
|
|
|
368
|
-
response = file_service.delete_from_recycle(req_data.id_list)
|
|
431
|
+
response = file_service.delete_from_recycle(user_email, req_data.id_list)
|
|
369
432
|
|
|
370
433
|
return web.json_response(response.to_dict())
|
|
371
434
|
|
|
@@ -376,9 +439,10 @@ async def handle_recycle_revert(request: web.Request) -> web.Response:
|
|
|
376
439
|
# Purpose: Restore items from recycle bin.
|
|
377
440
|
|
|
378
441
|
req_data = RecycleFileRequest.from_dict(await request.json())
|
|
442
|
+
user_email = request["user"]
|
|
379
443
|
file_service: FileService = request.app["file_service"]
|
|
380
444
|
|
|
381
|
-
response = file_service.revert_from_recycle(req_data.id_list)
|
|
445
|
+
response = file_service.revert_from_recycle(user_email, req_data.id_list)
|
|
382
446
|
|
|
383
447
|
return web.json_response(response.to_dict())
|
|
384
448
|
|
|
@@ -388,9 +452,10 @@ async def handle_recycle_clear(request: web.Request) -> web.Response:
|
|
|
388
452
|
# Endpoint: POST /api/file/recycle/clear
|
|
389
453
|
# Purpose: Empty the recycle bin.
|
|
390
454
|
|
|
455
|
+
user_email = request["user"]
|
|
391
456
|
file_service: FileService = request.app["file_service"]
|
|
392
457
|
|
|
393
|
-
response = file_service.clear_recycle()
|
|
458
|
+
response = file_service.clear_recycle(user_email)
|
|
394
459
|
|
|
395
460
|
return web.json_response(response.to_dict())
|
|
396
461
|
|
|
@@ -401,9 +466,10 @@ async def handle_file_search(request: web.Request) -> web.Response:
|
|
|
401
466
|
# Purpose: Search for files by keyword.
|
|
402
467
|
|
|
403
468
|
req_data = FileSearchRequest.from_dict(await request.json())
|
|
469
|
+
user_email = request["user"]
|
|
404
470
|
file_service: FileService = request.app["file_service"]
|
|
405
471
|
|
|
406
|
-
results = file_service.search_files(req_data.keyword)
|
|
472
|
+
results = file_service.search_files(user_email, req_data.keyword)
|
|
407
473
|
|
|
408
474
|
response = FileSearchResponse(entries=results)
|
|
409
475
|
|