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.
Files changed (50) hide show
  1. {supernote-0.5.0 → supernote-0.6.0}/PKG-INFO +9 -5
  2. {supernote-0.5.0 → supernote-0.6.0}/README.md +8 -4
  3. {supernote-0.5.0 → supernote-0.6.0}/pyproject.toml +1 -1
  4. supernote-0.6.0/supernote/cli/server.py +137 -0
  5. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/app.py +1 -0
  6. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/config.py +2 -2
  7. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/file.py +11 -0
  8. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/file.py +99 -33
  9. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/file.py +76 -98
  10. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/storage.py +101 -132
  11. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/user.py +15 -6
  12. {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/PKG-INFO +9 -5
  13. supernote-0.5.0/supernote/cli/server.py +0 -100
  14. {supernote-0.5.0 → supernote-0.6.0}/LICENSE +0 -0
  15. {supernote-0.5.0 → supernote-0.6.0}/setup.cfg +0 -0
  16. {supernote-0.5.0 → supernote-0.6.0}/supernote/__init__.py +0 -0
  17. {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/__init__.py +0 -0
  18. {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/client.py +0 -0
  19. {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/main.py +0 -0
  20. {supernote-0.5.0 → supernote-0.6.0}/supernote/cli/notebook.py +0 -0
  21. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/__init__.py +0 -0
  22. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/api_model.py +0 -0
  23. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/auth.py +0 -0
  24. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/client.py +0 -0
  25. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/cloud_client.py +0 -0
  26. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/exceptions.py +0 -0
  27. {supernote-0.5.0 → supernote-0.6.0}/supernote/client/login_client.py +0 -0
  28. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/__init__.py +0 -0
  29. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/color.py +0 -0
  30. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/converter.py +0 -0
  31. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/decoder.py +0 -0
  32. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/exceptions.py +0 -0
  33. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/fileformat.py +0 -0
  34. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/manipulator.py +0 -0
  35. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/parser.py +0 -0
  36. {supernote-0.5.0 → supernote-0.6.0}/supernote/notebook/utils.py +0 -0
  37. {supernote-0.5.0 → supernote-0.6.0}/supernote/py.typed +0 -0
  38. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/__init__.py +0 -0
  39. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/__init__.py +0 -0
  40. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/auth.py +0 -0
  41. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/models/base.py +0 -0
  42. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/auth.py +0 -0
  43. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/decorators.py +0 -0
  44. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/routes/system.py +0 -0
  45. {supernote-0.5.0 → supernote-0.6.0}/supernote/server/services/__init__.py +0 -0
  46. {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/SOURCES.txt +0 -0
  47. {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/dependency_links.txt +0 -0
  48. {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/entry_points.txt +0 -0
  49. {supernote-0.5.0 → supernote-0.6.0}/supernote.egg-info/requires.txt +0 -0
  50. {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.5.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
- # Configure users (you will be prompted for a password)
86
- supernote-server user add alice
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
- # Create user
101
+ # Generate initial configuration
99
102
  mkdir config
100
- docker run --rm -it -v $(pwd)/config:/config supernote-server supernote-server user add alice
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
- # Configure users (you will be prompted for a password)
56
- supernote-server user add alice
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
- # Create user
71
+ # Generate initial configuration
69
72
  mkdir config
70
- docker run --rm -it -v $(pwd)/config:/config supernote-server supernote-server user add alice
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.5.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 absolute
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=True, # True for normal sync, False for full re-upload
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
- SyncStartRequest.from_dict(await request.json())
57
- return web.json_response(BaseResponse().to_dict())
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(None, file_service.list_folder, path_str)
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(None, storage_service.get_storage_usage)
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(None, file_service.get_file_info, path_str)
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(None, file_service.get_file_info, file_id)
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": # type: ignore[union-attr]
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(filename, field.read_chunk) # type: ignore[union-attr]
196
- logger.info(f"Received upload for {filename}: {total_bytes} bytes")
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
- req_data.order, req_data.sequence, req_data.page_no, req_data.page_size
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