lfss 0.9.1__py3-none-any.whl → 0.9.4__py3-none-any.whl

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.
lfss/svc/common_impl.py CHANGED
@@ -7,7 +7,7 @@ from ..eng.datatype import UserRecord, FileRecord, PathContents, AccessLevel, Fi
7
7
  from ..eng.database import FileConn, UserConn, delayed_log_access, check_file_read_permission, check_path_permission
8
8
  from ..eng.thumb import get_thumb
9
9
  from ..eng.utils import format_last_modified, ensure_uri_compnents
10
- from ..eng.config import CHUNK_SIZE
10
+ from ..eng.config import CHUNK_SIZE, DEBUG_MODE
11
11
 
12
12
  from .app_base import skip_request_log, db, logger
13
13
 
@@ -60,10 +60,15 @@ async def emit_file(
60
60
  else:
61
61
  arng_e = range_end
62
62
 
63
- if arng_s >= file_record.file_size or arng_e >= file_record.file_size:
64
- raise HTTPException(status_code=416, detail="Range not satisfiable")
65
- if arng_s > arng_e:
66
- raise HTTPException(status_code=416, detail="Invalid range")
63
+ if file_record.file_size > 0:
64
+ if arng_s >= file_record.file_size or arng_e >= file_record.file_size:
65
+ if DEBUG_MODE: print(f"[Invalid range] Actual range: {arng_s}-{arng_e} (size: {file_record.file_size})")
66
+ raise HTTPException(status_code=416, detail="Range not satisfiable")
67
+ if arng_s > arng_e:
68
+ raise HTTPException(status_code=416, detail="Invalid range")
69
+ else:
70
+ if not (arng_s == 0 and arng_e == -1):
71
+ raise HTTPException(status_code=416, detail="Invalid range (file size is 0)")
67
72
 
68
73
  headers = {
69
74
  "Content-Disposition": f"{disposition}; filename={fname}",
@@ -87,7 +92,7 @@ async def emit_file(
87
92
  status_code=206 if range_start != -1 or range_end != -1 else 200
88
93
  )
89
94
 
90
- async def get_file_impl(
95
+ async def get_impl(
91
96
  request: Request,
92
97
  user: UserRecord,
93
98
  path: str,
@@ -96,30 +101,12 @@ async def get_file_impl(
96
101
  is_head = False,
97
102
  ):
98
103
  path = ensure_uri_compnents(path)
104
+ if path.startswith("/"): path = path[1:]
99
105
 
100
106
  # handle directory query
101
107
  if path == "": path = "/"
102
108
  if path.endswith("/"):
103
- # return file under the path as json
104
- async with unique_cursor() as cur:
105
- fconn = FileConn(cur)
106
- if user.id == 0:
107
- raise HTTPException(status_code=401, detail="Permission denied, credential required")
108
- if thumb:
109
- return await emit_thumbnail(path, download, create_time=None)
110
-
111
- if path == "/":
112
- peer_users = await UserConn(cur).list_peer_users(user.id, AccessLevel.READ)
113
- return PathContents(
114
- dirs = await fconn.list_root_dirs(user.username, *[x.username for x in peer_users], skim=True) \
115
- if not user.is_admin else await fconn.list_root_dirs(skim=True),
116
- files = []
117
- )
118
-
119
- if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
120
- raise HTTPException(status_code=403, detail="Permission denied")
121
-
122
- return await fconn.list_path(path)
109
+ return await _get_dir_impl(user=user, path=path, download=download, thumb=thumb, is_head=is_head)
123
110
 
124
111
  # handle file query
125
112
  async with unique_cursor() as cur:
@@ -147,6 +134,9 @@ async def get_file_impl(
147
134
  else:
148
135
  range_start, range_end = -1, -1
149
136
 
137
+ if DEBUG_MODE:
138
+ print(f"Get range: {range_start}-{range_end}")
139
+
150
140
  if thumb:
151
141
  if (range_start != -1 or range_end != -1): logger.warning("Range request for thumbnail")
152
142
  return await emit_thumbnail(path, download, create_time=file_record.create_time, is_head=is_head)
@@ -156,11 +146,55 @@ async def get_file_impl(
156
146
  else:
157
147
  return await emit_file(file_record, None, "inline", is_head = is_head, range_start=range_start, range_end=range_end)
158
148
 
149
+ async def _get_dir_impl(
150
+ user: UserRecord,
151
+ path: str,
152
+ download: bool = False,
153
+ thumb: bool = False,
154
+ is_head = False,
155
+ ):
156
+ """ handle directory query, return file under the path as json """
157
+ assert path.endswith("/")
158
+ async with unique_cursor() as cur:
159
+ fconn = FileConn(cur)
160
+ if user.id == 0:
161
+ raise HTTPException(status_code=401, detail="Permission denied, credential required")
162
+ if thumb:
163
+ return await emit_thumbnail(path, download, create_time=None)
164
+
165
+ if path == "/":
166
+ if is_head: return Response(status_code=200)
167
+ peer_users = await UserConn(cur).list_peer_users(user.id, AccessLevel.READ)
168
+ return PathContents(
169
+ dirs = await fconn.list_root_dirs(user.username, *[x.username for x in peer_users], skim=True) \
170
+ if not user.is_admin else await fconn.list_root_dirs(skim=True),
171
+ files = []
172
+ )
173
+
174
+ if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
175
+ raise HTTPException(status_code=403, detail="Permission denied")
176
+
177
+ path_sp = path.split("/")
178
+ if is_head:
179
+ if len(path_sp) == 2:
180
+ assert path_sp[1] == ""
181
+ if await UserConn(cur).get_user(path_sp[0]):
182
+ return Response(status_code=200)
183
+ else:
184
+ raise HTTPException(status_code=404, detail="User not found")
185
+ else:
186
+ if await FileConn(cur).count_path_files(path, flat=True) > 0:
187
+ return Response(status_code=200)
188
+ else:
189
+ raise HTTPException(status_code=404, detail="Path not found")
190
+
191
+ return await fconn.list_path(path)
192
+
159
193
  async def put_file_impl(
160
194
  request: Request,
161
195
  user: UserRecord,
162
196
  path: str,
163
- conflict: Literal["overwrite", "skip", "abort"] = "abort",
197
+ conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
164
198
  permission: int = 0,
165
199
  ):
166
200
  path = ensure_uri_compnents(path)
@@ -187,7 +221,9 @@ async def put_file_impl(
187
221
  exists_flag = True
188
222
  if await check_path_permission(path, user) < AccessLevel.WRITE:
189
223
  raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
190
- await db.delete_file(path)
224
+ old_record = await db.delete_file(path)
225
+ if old_record and permission == FileReadPermission.UNSET.value:
226
+ permission = old_record.permission.value # inherit permission
191
227
 
192
228
  # check content-type
193
229
  content_type = request.headers.get("Content-Type", "application/octet-stream")
@@ -213,7 +249,7 @@ async def post_file_impl(
213
249
  path: str,
214
250
  user: UserRecord,
215
251
  file: UploadFile,
216
- conflict: Literal["overwrite", "skip", "abort"] = "abort",
252
+ conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
217
253
  permission: int = 0,
218
254
  ):
219
255
  path = ensure_uri_compnents(path)
@@ -240,7 +276,9 @@ async def post_file_impl(
240
276
  exists_flag = True
241
277
  if await check_path_permission(path, user) < AccessLevel.WRITE:
242
278
  raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
243
- await db.delete_file(path)
279
+ old_record = await db.delete_file(path)
280
+ if old_record and permission == FileReadPermission.UNSET.value:
281
+ permission = old_record.permission.value # inherit permission
244
282
 
245
283
  async def blob_reader():
246
284
  nonlocal file
@@ -252,7 +290,7 @@ async def post_file_impl(
252
290
  "Content-Type": "application/json",
253
291
  }, content=json.dumps({"url": path}))
254
292
 
255
- async def delete_file_impl(path: str, user: UserRecord):
293
+ async def delete_impl(path: str, user: UserRecord):
256
294
  path = ensure_uri_compnents(path)
257
295
  if await check_path_permission(path, user) < AccessLevel.WRITE:
258
296
  raise HTTPException(status_code=403, detail="Permission denied")
@@ -268,3 +306,32 @@ async def delete_file_impl(path: str, user: UserRecord):
268
306
  return Response(status_code=200, content="Deleted")
269
307
  else:
270
308
  return Response(status_code=404, content="Not found")
309
+
310
+ async def copy_impl(
311
+ op_user: UserRecord, src_path: str, dst_path: str,
312
+ ):
313
+ src_path = ensure_uri_compnents(src_path)
314
+ dst_path = ensure_uri_compnents(dst_path)
315
+ copy_type = "file" if not src_path[-1] == "/" else "directory"
316
+ if (src_path[-1] == "/") != (dst_path[-1] == "/"):
317
+ raise HTTPException(status_code=400, detail="Source and destination must be same type")
318
+
319
+ if src_path == dst_path:
320
+ raise HTTPException(status_code=400, detail="Source and destination are the same")
321
+
322
+ logger.info(f"Copy {src_path} to {dst_path}, user: {op_user.username}")
323
+ if copy_type == "file":
324
+ async with unique_cursor() as cur:
325
+ fconn = FileConn(cur)
326
+ dst_record = await fconn.get_file_record(dst_path)
327
+ if dst_record:
328
+ raise HTTPException(status_code=409, detail="Destination exists")
329
+ await db.copy_file(src_path, dst_path, op_user)
330
+ else:
331
+ async with unique_cursor() as cur:
332
+ fconn = FileConn(cur)
333
+ dst_fcount = await fconn.count_path_files(dst_path, flat=True)
334
+ if dst_fcount > 0:
335
+ raise HTTPException(status_code=409, detail="Destination exists")
336
+ await db.copy_path(src_path, dst_path, op_user)
337
+ return Response(status_code=201, content="OK")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.9.1
3
+ Version: 0.9.4
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li_mengxun
@@ -57,10 +57,12 @@ The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/ur
57
57
  The authentication can be acheived through one of the following methods:
58
58
  1. `Authorization` header with the value `Bearer sha256(<username><password>)`.
59
59
  2. `token` query parameter with the value `sha256(<username><password>)`.
60
- 3. HTTP Basic Authentication with the username and password.
60
+ 3. HTTP Basic Authentication with the username and password (If WebDAV is enabled).
61
61
 
62
62
  You can refer to `frontend` as an application example, `lfss/api/connector.py` for more APIs.
63
63
 
64
64
  By default, the service exposes all files to the public for `GET` requests,
65
65
  but file-listing is restricted to the user's own files.
66
- Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
66
+ Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
67
+
68
+ More can be found in the [docs](./docs) directory.
@@ -1,8 +1,10 @@
1
- Readme.md,sha256=6gOvhb93ma83VKC4-pfi4TccZxCq4kj3GEB9qa9ols4,1759
2
- docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
1
+ Readme.md,sha256=JVe9T6N1Rz4hTiiCVoDYe2VB0dAi60VcBgb2twQdfZc,1834
2
+ docs/Changelog.md,sha256=3mRHcda4UK8c105XtBfbeTWij0S4xNc-U8JTTPUqCJk,769
3
+ docs/Enviroment_variables.md,sha256=LUZF1o70emp-5UPsvXPjcxapP940OqEZzSyyUUT9bEQ,569
4
+ docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
3
5
  docs/Permission.md,sha256=mvK8gVBBgoIFJqikcaReU_bUo-mTq_ECqJaDDJoQF7Q,3126
4
- docs/Webdav.md,sha256=9Q41ROEJodVVAnlo1Tf0jqsyrbuHhv_ElSsXbIPXYIg,1547
5
- frontend/api.js,sha256=hMV6Fc1JxkFQgv7BV1Y_Su7pqsWeF_92hPMmDBcXC04,18485
6
+ docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
7
+ frontend/api.js,sha256=GlQsNoZFEcy7QUUsLbXv7aP-KxRnIxM37FQHTaakGiQ,19387
6
8
  frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
7
9
  frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
8
10
  frontend/info.js,sha256=xGUJPCSrtDhuSu0ELLQZ77PmVWldg-prU1mwQGbdEoA,5797
@@ -10,14 +12,14 @@ frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
10
12
  frontend/login.js,sha256=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
11
13
  frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
12
14
  frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
13
- frontend/scripts.js,sha256=nWH6NgavZTVmjK44i2DeRi6mJzGSe4qeQPUbDaEVt58,21735
15
+ frontend/scripts.js,sha256=2-Omsb1-s4Wc859_SYw8JGyeUSiADaH9va4w87Mozns,24134
14
16
  frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
15
17
  frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
16
18
  frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
17
19
  frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
18
20
  frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
19
21
  lfss/api/__init__.py,sha256=8IJqrpWK1doIyVVbntvVic82A57ncwl5b0BRHX4Ri6A,6660
20
- lfss/api/connector.py,sha256=gLn-eW1m6trjqj54YXzPqByQFT56WlSy08kUm1UX4LE,11573
22
+ lfss/api/connector.py,sha256=hHSEEWecKQGZH6oxAmYoG3q7lFfacCbOKVZiUIXT2y8,11819
21
23
  lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
22
24
  lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
23
25
  lfss/cli/cli.py,sha256=aYjB8d4k6JUd9efxZK-XOj-mlG4JeOr_0lnj2qqCiK0,8066
@@ -28,22 +30,22 @@ lfss/cli/vacuum.py,sha256=GOG72d3NYe9bYCNc3y8JecEmM-DrKlGq3JQcisv_xBg,3702
28
30
  lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
31
  lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
30
32
  lfss/eng/config.py,sha256=DmnUYMeLOL-45OstysyMpSBPmLofgzvcSrsWjHvssYs,915
31
- lfss/eng/connection_pool.py,sha256=-tePasJxiZZ73ymgWf_kFnaKouc4Rrr4K6EXwjb7Mm4,6141
32
- lfss/eng/database.py,sha256=81wp6LczdByk05RYcJfLjd0tx4ZT3Ue2k994UYMiDZI,47172
33
+ lfss/eng/connection_pool.py,sha256=4xOF1kXXGqCWeLX5ZVFALKjdY8N1VVAVSSTRfCzbj94,6141
34
+ lfss/eng/database.py,sha256=2i8gbh1odOA09tS5VU9cUZy3poZUdCx3XX7UX7umtxw,47188
33
35
  lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
34
- lfss/eng/error.py,sha256=61hcjeQ-y5htKhImDtDuWDf0GPrwbj5LySibeHiKFxc,529
36
+ lfss/eng/error.py,sha256=dAlQHXOnQcSkA2vTugJFSxcyDqoFlPucBoFpTZ7GI6w,654
35
37
  lfss/eng/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
36
- lfss/eng/thumb.py,sha256=YO1yTI8WzW7pBpQN9x5PtPayxhftb32IJl1zPSS9mks,3243
37
- lfss/eng/utils.py,sha256=zZ7r9BsNV8XJJVNOxfIqRCO1bxNzh7bc9vEJiCkgbKI,6208
38
+ lfss/eng/thumb.py,sha256=x9jIHHU1tskmp-TavPPcxGpbmEjCp9gbH6ZlsEfqUxY,3383
39
+ lfss/eng/utils.py,sha256=CYEQvPiM28k53hCJBE7N6O6a1xC_wvnP3KZx4DCnD0k,6723
38
40
  lfss/sql/init.sql,sha256=8LjHx0TBCkBD62xFfssSeHDqKYVQQJkZAg4rSm046f4,1496
39
41
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
40
- lfss/svc/app.py,sha256=XK0hx5yKMk8JViJ2BXgsFr3hgSdWn_equJ0JoJwMiuc,221
41
- lfss/svc/app_base.py,sha256=LjH9tJzaFcP7PB8OBUizG9kvNhts2eq0JxNMBUCoHw0,6312
42
- lfss/svc/app_dav.py,sha256=8Etdoh_NmvZpEG8J9m97xRF4i-GNGJNvNfgjfLIDjzk,17224
43
- lfss/svc/app_native.py,sha256=ML-PR-zdoi5j3-KrGhIaYPMbSoW9Lesh7NROPNPUINU,9221
44
- lfss/svc/common_impl.py,sha256=m3lMA97a4yd_VFG3IrVYIsDzyUEseX7sU633qj7iyDI,10858
42
+ lfss/svc/app.py,sha256=ftWCpepBx-gTSG7i-TB-IdinPPstAYYQjCgnTfeMZeI,219
43
+ lfss/svc/app_base.py,sha256=BU_DndHW4sYiWUQcTis8iGljmUy8FHfZrzCkE0d1z-Y,6717
44
+ lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
45
+ lfss/svc/app_native.py,sha256=6yBRJB8_p4RZgDVheDTv1ClBGc3etrQm94j1NiR4FUQ,9349
46
+ lfss/svc/common_impl.py,sha256=0fjbqHWgqDhLfBEu6aC0Z5qgNt67C7z0Qroj7aV3Iq4,13830
45
47
  lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
46
- lfss-0.9.1.dist-info/METADATA,sha256=CZuVoVTKJG002grUK_hXtgy0b_j3m-eNHDK0SRyVLL4,2519
47
- lfss-0.9.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
48
- lfss-0.9.1.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
49
- lfss-0.9.1.dist-info/RECORD,,
48
+ lfss-0.9.4.dist-info/METADATA,sha256=3wUuwMRn55Z2lnX9wZRGMVxLbfphSLOk1gX01haFaOw,2594
49
+ lfss-0.9.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
+ lfss-0.9.4.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
51
+ lfss-0.9.4.dist-info/RECORD,,
File without changes