lfss 0.5.1__tar.gz → 0.5.2__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.
- {lfss-0.5.1 → lfss-0.5.2}/PKG-INFO +1 -1
- {lfss-0.5.1 → lfss-0.5.2}/frontend/api.js +1 -1
- {lfss-0.5.1 → lfss-0.5.2}/frontend/popup.js +13 -1
- {lfss-0.5.1 → lfss-0.5.2}/frontend/scripts.js +38 -6
- {lfss-0.5.1 → lfss-0.5.2}/frontend/styles.css +1 -1
- {lfss-0.5.1 → lfss-0.5.2}/lfss/cli/balance.py +37 -24
- {lfss-0.5.1 → lfss-0.5.2}/lfss/client/api.py +27 -8
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/database.py +27 -1
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/server.py +50 -18
- {lfss-0.5.1 → lfss-0.5.2}/pyproject.toml +1 -1
- {lfss-0.5.1 → lfss-0.5.2}/Readme.md +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/docs/Known_issues.md +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/docs/Permission.md +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/frontend/index.html +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/frontend/popup.css +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/frontend/utils.js +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/cli/cli.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/cli/panel.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/cli/serve.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/cli/user.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/client/__init__.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/sql/init.sql +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/sql/pragma.sql +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/__init__.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/config.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/error.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/log.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/stat.py +0 -0
- {lfss-0.5.1 → lfss-0.5.2}/lfss/src/utils.py +0 -0
@@ -191,7 +191,7 @@ export default class Connector {
|
|
191
191
|
* @param {string} path - file path(url)
|
192
192
|
* @param {string} newPath - new file path(url)
|
193
193
|
*/
|
194
|
-
async
|
194
|
+
async move(path, newPath){
|
195
195
|
if (path.startsWith('/')){ path = path.slice(1); }
|
196
196
|
if (newPath.startsWith('/')){ newPath = newPath.slice(1); }
|
197
197
|
const dst = new URL(this.config.endpoint + '/_api/meta');
|
@@ -40,7 +40,7 @@ export function createFloatingWindow(innerHTML = '', {
|
|
40
40
|
return [floatingWindow, closeWindow];
|
41
41
|
}
|
42
42
|
|
43
|
-
/* select can be "last-filename" */
|
43
|
+
/* select can be "last-filename" or "last-pathname" */
|
44
44
|
export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
|
45
45
|
text = "",
|
46
46
|
placeholder = "Enter text",
|
@@ -72,6 +72,7 @@ export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
|
|
72
72
|
};
|
73
73
|
|
74
74
|
if (select === "last-filename") {
|
75
|
+
// select the last filename, e.g. "file" in "/path/to/file.txt"
|
75
76
|
const inputVal = input.value;
|
76
77
|
let lastSlash = inputVal.lastIndexOf("/");
|
77
78
|
if (lastSlash === -1) {
|
@@ -84,6 +85,17 @@ export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
|
|
84
85
|
}
|
85
86
|
input.setSelectionRange(lastSlash + 1, lastSlash + lastDot + 1);
|
86
87
|
}
|
88
|
+
else if (select === "last-pathname") {
|
89
|
+
// select the last pathname, e.g. "to" in "/path/to/<filename>"
|
90
|
+
const lastSlash = input.value.lastIndexOf("/");
|
91
|
+
const secondLastSlash = input.value.lastIndexOf("/", input.value.lastIndexOf("/") - 1);
|
92
|
+
if (secondLastSlash !== -1) {
|
93
|
+
input.setSelectionRange(secondLastSlash + 1, lastSlash);
|
94
|
+
}
|
95
|
+
else {
|
96
|
+
input.setSelectionRange(0, lastSlash);
|
97
|
+
}
|
98
|
+
}
|
87
99
|
|
88
100
|
return [floatingWindow, closeWindow];
|
89
101
|
}
|
@@ -5,6 +5,9 @@ import { formatSize, decodePathURI, ensurePathURI, copyToClipboard, getRandomStr
|
|
5
5
|
|
6
6
|
const conn = new Connector();
|
7
7
|
let userRecord = null;
|
8
|
+
const ensureSlashEnd = (path) => {
|
9
|
+
return path.endsWith('/') ? path : path + '/';
|
10
|
+
}
|
8
11
|
|
9
12
|
const endpointInput = document.querySelector('input#endpoint');
|
10
13
|
const tokenInput = document.querySelector('input#token');
|
@@ -128,11 +131,13 @@ uploadButton.addEventListener('click', () => {
|
|
128
131
|
throw new Error('File name cannot end with /');
|
129
132
|
}
|
130
133
|
path = path + fileName;
|
134
|
+
showPopup('Uploading...', {level: 'info', timeout: 3000});
|
131
135
|
conn.put(path, file)
|
132
136
|
.then(() => {
|
133
137
|
refreshFileList();
|
134
138
|
uploadFileNameInput.value = '';
|
135
139
|
onFileNameInpuChange();
|
140
|
+
showPopup('Upload success.', {level: 'success', timeout: 3000});
|
136
141
|
},
|
137
142
|
(err) => {
|
138
143
|
showPopup('Failed to upload file: ' + err, {level: 'error', timeout: 5000});
|
@@ -191,9 +196,14 @@ Are you sure you want to proceed?
|
|
191
196
|
const path = dstPath + file.name;
|
192
197
|
promises.push(uploadFile(file, path));
|
193
198
|
}
|
199
|
+
showPopup('Uploading multiple files...', {level: 'info', timeout: 3000});
|
194
200
|
Promise.all(promises).then(
|
195
201
|
() => {
|
202
|
+
showPopup('Upload success.', {level: 'success', timeout: 3000});
|
196
203
|
refreshFileList();
|
204
|
+
},
|
205
|
+
(err) => {
|
206
|
+
showPopup('Failed to upload some files: ' + err, {level: 'error', timeout: 5000});
|
197
207
|
}
|
198
208
|
);
|
199
209
|
}
|
@@ -260,15 +270,16 @@ function refreshFileList(){
|
|
260
270
|
tr.appendChild(accessTd);
|
261
271
|
}
|
262
272
|
{
|
273
|
+
const dirurl = ensureSlashEnd(dir.url);
|
263
274
|
const actTd = document.createElement('td');
|
264
275
|
const actContainer = document.createElement('div');
|
265
276
|
actContainer.classList.add('action-container');
|
266
277
|
|
267
278
|
const showMetaButton = document.createElement('a');
|
268
|
-
showMetaButton.textContent = '
|
279
|
+
showMetaButton.textContent = 'Reveal';
|
269
280
|
showMetaButton.style.cursor = 'pointer';
|
270
281
|
showMetaButton.addEventListener('click', () => {
|
271
|
-
const dirUrlEncap =
|
282
|
+
const dirUrlEncap = dirurl;
|
272
283
|
conn.getMetadata(dirUrlEncap).then(
|
273
284
|
(meta) => {
|
274
285
|
sizeTd.textContent = formatSize(meta.size);
|
@@ -280,6 +291,30 @@ function refreshFileList(){
|
|
280
291
|
});
|
281
292
|
actContainer.appendChild(showMetaButton);
|
282
293
|
|
294
|
+
const moveButton = document.createElement('a');
|
295
|
+
moveButton.textContent = 'Move';
|
296
|
+
moveButton.style.cursor = 'pointer';
|
297
|
+
moveButton.addEventListener('click', () => {
|
298
|
+
showFloatingWindowLineInput((dstPath) => {
|
299
|
+
dstPath = encodePathURI(dstPath);
|
300
|
+
console.log("Moving", dirurl, "to", dstPath);
|
301
|
+
conn.move(dirurl, dstPath)
|
302
|
+
.then(() => {
|
303
|
+
refreshFileList();
|
304
|
+
},
|
305
|
+
(err) => {
|
306
|
+
showPopup('Failed to move path: ' + err, {level: 'error'});
|
307
|
+
}
|
308
|
+
);
|
309
|
+
}, {
|
310
|
+
text: 'Enter the destination path: ',
|
311
|
+
placeholder: 'Destination path',
|
312
|
+
value: decodePathURI(dirurl),
|
313
|
+
select: "last-pathname"
|
314
|
+
});
|
315
|
+
});
|
316
|
+
actContainer.appendChild(moveButton);
|
317
|
+
|
283
318
|
const downloadButton = document.createElement('a');
|
284
319
|
downloadButton.textContent = 'Download';
|
285
320
|
downloadButton.href = conn.config.endpoint + '/_api/bundle?' +
|
@@ -402,10 +437,7 @@ function refreshFileList(){
|
|
402
437
|
moveButton.addEventListener('click', () => {
|
403
438
|
showFloatingWindowLineInput((dstPath) => {
|
404
439
|
dstPath = encodePathURI(dstPath);
|
405
|
-
|
406
|
-
dstPath = dstPath.slice(0, -1);
|
407
|
-
}
|
408
|
-
conn.moveFile(file.url, dstPath)
|
440
|
+
conn.move(file.url, dstPath)
|
409
441
|
.then(() => {
|
410
442
|
refreshFileList();
|
411
443
|
},
|
@@ -68,44 +68,57 @@ async def move_to_internal(f_id: str, flag: str = ''):
|
|
68
68
|
raise e
|
69
69
|
|
70
70
|
|
71
|
-
async def _main():
|
71
|
+
async def _main(batch_size: int = 10000):
|
72
72
|
|
73
73
|
tasks = []
|
74
74
|
start_time = time.time()
|
75
|
-
async with aiosqlite.connect(db_file) as conn:
|
76
|
-
exceeded_rows = await (await conn.execute(
|
77
|
-
"SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0",
|
78
|
-
(LARGE_FILE_BYTES,)
|
79
|
-
)).fetchall()
|
80
|
-
|
81
|
-
for i in range(0, len(exceeded_rows)):
|
82
|
-
row = exceeded_rows[i]
|
83
|
-
f_id = row[0]
|
84
|
-
tasks.append(move_to_external(f_id, flag=f"[e-{i+1}/{len(exceeded_rows)}] "))
|
85
75
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
76
|
+
e_cout = 0
|
77
|
+
batch_count = 0
|
78
|
+
while True:
|
79
|
+
async with aiosqlite.connect(db_file) as conn:
|
80
|
+
exceeded_rows = list(await (await conn.execute(
|
81
|
+
"SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0 LIMIT ? OFFSET ?",
|
82
|
+
(LARGE_FILE_BYTES, batch_size, batch_size * batch_count)
|
83
|
+
)).fetchall())
|
84
|
+
if not exceeded_rows:
|
85
|
+
break
|
86
|
+
e_cout += len(exceeded_rows)
|
87
|
+
for i in range(0, len(exceeded_rows)):
|
88
|
+
row = exceeded_rows[i]
|
89
|
+
f_id = row[0]
|
90
|
+
tasks.append(move_to_external(f_id, flag=f"[b{batch_count+1}-e{i+1}/{len(exceeded_rows)}] "))
|
91
|
+
await asyncio.gather(*tasks)
|
92
|
+
|
93
|
+
i_count = 0
|
94
|
+
batch_count = 0
|
95
|
+
while True:
|
96
|
+
async with aiosqlite.connect(db_file) as conn:
|
97
|
+
under_rows = list(await (await conn.execute(
|
98
|
+
"SELECT file_id, file_size, external FROM fmeta WHERE file_size <= ? AND external = 1 LIMIT ? OFFSET ?",
|
99
|
+
(LARGE_FILE_BYTES, batch_size, batch_size * batch_count)
|
100
|
+
)).fetchall())
|
101
|
+
if not under_rows:
|
102
|
+
break
|
103
|
+
i_count += len(under_rows)
|
104
|
+
for i in range(0, len(under_rows)):
|
105
|
+
row = under_rows[i]
|
106
|
+
f_id = row[0]
|
107
|
+
tasks.append(move_to_internal(f_id, flag=f"[b{batch_count+1}-i{i+1}/{len(under_rows)}] "))
|
108
|
+
await asyncio.gather(*tasks)
|
91
109
|
|
92
|
-
for i in range(0, len(under_rows)):
|
93
|
-
row = under_rows[i]
|
94
|
-
f_id = row[0]
|
95
|
-
tasks.append(move_to_internal(f_id, flag=f"[i-{i+1}/{len(under_rows)}] "))
|
96
|
-
|
97
|
-
await asyncio.gather(*tasks)
|
98
110
|
end_time = time.time()
|
99
111
|
print(f"Balancing complete, took {end_time - start_time:.2f} seconds. "
|
100
|
-
f"{
|
112
|
+
f"{e_cout} files moved to external storage, {i_count} files moved to internal storage.")
|
101
113
|
|
102
114
|
def main():
|
103
115
|
global sem
|
104
116
|
parser = argparse.ArgumentParser(description="Balance the storage by ensuring that large file thresholds are met.")
|
105
117
|
parser.add_argument("-j", "--jobs", type=int, default=2, help="Number of concurrent jobs")
|
118
|
+
parser.add_argument("-b", "--batch-size", type=int, default=10000, help="Batch size for processing files")
|
106
119
|
args = parser.parse_args()
|
107
120
|
sem = Semaphore(args.jobs)
|
108
|
-
asyncio.run(_main())
|
121
|
+
asyncio.run(_main(args.batch_size))
|
109
122
|
|
110
123
|
if __name__ == '__main__':
|
111
124
|
main()
|
@@ -36,8 +36,6 @@ class Connector:
|
|
36
36
|
|
37
37
|
def put(self, path: str, file_data: bytes, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip'] = 'abort'):
|
38
38
|
"""Uploads a file to the specified path."""
|
39
|
-
if path.startswith('/'):
|
40
|
-
path = path[1:]
|
41
39
|
response = self._fetch('PUT', path, search_params={
|
42
40
|
'permission': int(permission),
|
43
41
|
'conflict': conflict
|
@@ -47,20 +45,41 @@ class Connector:
|
|
47
45
|
)
|
48
46
|
return response.json()
|
49
47
|
|
50
|
-
def
|
51
|
-
"""
|
48
|
+
def put_json(self, path: str, data: dict, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip'] = 'abort'):
|
49
|
+
"""Uploads a JSON file to the specified path."""
|
50
|
+
assert path.endswith('.json'), "Path must end with .json"
|
51
|
+
response = self._fetch('PUT', path, search_params={
|
52
|
+
'permission': int(permission),
|
53
|
+
'conflict': conflict
|
54
|
+
})(
|
55
|
+
json=data,
|
56
|
+
headers={'Content-Type': 'application/json'}
|
57
|
+
)
|
58
|
+
return response.json()
|
59
|
+
|
60
|
+
def _get(self, path: str) -> Optional[requests.Response]:
|
52
61
|
try:
|
53
62
|
response = self._fetch('GET', path)()
|
54
63
|
except requests.exceptions.HTTPError as e:
|
55
64
|
if e.response.status_code == 404:
|
56
65
|
return None
|
57
66
|
raise e
|
67
|
+
return response
|
68
|
+
|
69
|
+
def get(self, path: str) -> Optional[bytes]:
|
70
|
+
"""Downloads a file from the specified path."""
|
71
|
+
response = self._get(path)
|
72
|
+
if response is None: return None
|
58
73
|
return response.content
|
74
|
+
|
75
|
+
def get_json(self, path: str) -> Optional[dict]:
|
76
|
+
response = self._get(path)
|
77
|
+
if response is None: return None
|
78
|
+
assert response.headers['Content-Type'] == 'application/json'
|
79
|
+
return response.json()
|
59
80
|
|
60
81
|
def delete(self, path: str):
|
61
82
|
"""Deletes the file at the specified path."""
|
62
|
-
if path.startswith('/'):
|
63
|
-
path = path[1:]
|
64
83
|
self._fetch('DELETE', path)()
|
65
84
|
|
66
85
|
def get_metadata(self, path: str) -> Optional[FileRecord | DirectoryRecord]:
|
@@ -87,8 +106,8 @@ class Connector:
|
|
87
106
|
headers={'Content-Type': 'application/www-form-urlencoded'}
|
88
107
|
)
|
89
108
|
|
90
|
-
def
|
91
|
-
"""
|
109
|
+
def move(self, path: str, new_path: str):
|
110
|
+
"""Move file or directory to a new path."""
|
92
111
|
self._fetch('POST', '_api/meta', {'path': path, 'new_path': new_path})(
|
93
112
|
headers = {'Content-Type': 'application/www-form-urlencoded'}
|
94
113
|
)
|
@@ -331,7 +331,7 @@ class FileConn(DBConnBase):
|
|
331
331
|
dirs = await asyncio.gather(*[get_dir(url + d) for d in dirs_str])
|
332
332
|
return PathContents(dirs, files)
|
333
333
|
|
334
|
-
async def get_path_record(self, url: str) ->
|
334
|
+
async def get_path_record(self, url: str) -> DirectoryRecord:
|
335
335
|
assert url.endswith('/'), "Path must end with /"
|
336
336
|
async with self.conn.execute("""
|
337
337
|
SELECT MIN(create_time) as create_time,
|
@@ -421,6 +421,25 @@ class FileConn(DBConnBase):
|
|
421
421
|
async with self.conn.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_url, old_url)):
|
422
422
|
self.logger.info(f"Moved file {old_url} to {new_url}")
|
423
423
|
|
424
|
+
@atomic
|
425
|
+
async def move_path(self, old_url: str, new_url: str, conflict_handler: Literal['skip', 'overwrite'] = 'overwrite', user_id: Optional[int] = None):
|
426
|
+
assert old_url.endswith('/'), "Old path must end with /"
|
427
|
+
assert new_url.endswith('/'), "New path must end with /"
|
428
|
+
if user_id is None:
|
429
|
+
async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ?", (old_url + '%', )) as cursor:
|
430
|
+
res = await cursor.fetchall()
|
431
|
+
else:
|
432
|
+
async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ? AND owner_id = ?", (old_url + '%', user_id)) as cursor:
|
433
|
+
res = await cursor.fetchall()
|
434
|
+
for r in res:
|
435
|
+
new_r = new_url + r[0][len(old_url):]
|
436
|
+
if conflict_handler == 'overwrite':
|
437
|
+
await self.conn.execute("DELETE FROM fmeta WHERE url = ?", (new_r, ))
|
438
|
+
elif conflict_handler == 'skip':
|
439
|
+
if (await self.conn.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))) is not None:
|
440
|
+
continue
|
441
|
+
await self.conn.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_r, r[0]))
|
442
|
+
|
424
443
|
async def log_access(self, url: str):
|
425
444
|
await self.conn.execute("UPDATE fmeta SET access_time = CURRENT_TIMESTAMP WHERE url = ?", (url, ))
|
426
445
|
|
@@ -664,6 +683,13 @@ class Database:
|
|
664
683
|
|
665
684
|
async with transaction(self):
|
666
685
|
await self.file.move_file(old_url, new_url)
|
686
|
+
|
687
|
+
async def move_path(self, old_url: str, new_url: str, user_id: Optional[int] = None):
|
688
|
+
validate_url(old_url, is_file=False)
|
689
|
+
validate_url(new_url, is_file=False)
|
690
|
+
|
691
|
+
async with transaction(self):
|
692
|
+
await self.file.move_path(old_url, new_url, 'overwrite', user_id)
|
667
693
|
|
668
694
|
async def __batch_delete_file_blobs(self, file_records: list[FileRecord], batch_size: int = 512):
|
669
695
|
# https://github.com/langchain-ai/langchain/issues/10321
|
@@ -263,6 +263,7 @@ async def delete_file(path: str, user: UserRecord = Depends(get_current_user)):
|
|
263
263
|
else:
|
264
264
|
res = await conn.delete_file(path)
|
265
265
|
|
266
|
+
await conn.user.set_active(user.username)
|
266
267
|
if res:
|
267
268
|
return Response(status_code=200, content="Deleted")
|
268
269
|
else:
|
@@ -335,26 +336,57 @@ async def update_file_meta(
|
|
335
336
|
if user.id == 0:
|
336
337
|
raise HTTPException(status_code=401, detail="Permission denied")
|
337
338
|
path = ensure_uri_compnents(path)
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
if not (
|
344
|
-
|
345
|
-
|
339
|
+
if path.startswith("/"):
|
340
|
+
path = path[1:]
|
341
|
+
await conn.user.set_active(user.username)
|
342
|
+
|
343
|
+
# file
|
344
|
+
if not path.endswith("/"):
|
345
|
+
file_record = await conn.file.get_file_record(path)
|
346
|
+
if not file_record:
|
347
|
+
logger.debug(f"Reject update meta request from {user.username} to {path}")
|
348
|
+
raise HTTPException(status_code=404, detail="File not found")
|
349
|
+
|
350
|
+
if not (user.is_admin or user.id == file_record.owner_id):
|
351
|
+
logger.debug(f"Reject update meta request from {user.username} to {path}")
|
352
|
+
raise HTTPException(status_code=403, detail="Permission denied")
|
353
|
+
|
354
|
+
if perm is not None:
|
355
|
+
logger.info(f"Update permission of {path} to {perm}")
|
356
|
+
await conn.file.set_file_record(
|
357
|
+
url = file_record.url,
|
358
|
+
permission = FileReadPermission(perm)
|
359
|
+
)
|
346
360
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
permission = FileReadPermission(perm)
|
352
|
-
)
|
361
|
+
if new_path is not None:
|
362
|
+
new_path = ensure_uri_compnents(new_path)
|
363
|
+
logger.info(f"Update path of {path} to {new_path}")
|
364
|
+
await conn.move_file(path, new_path)
|
353
365
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
366
|
+
# directory
|
367
|
+
else:
|
368
|
+
assert perm is None, "Permission is not supported for directory"
|
369
|
+
if new_path is not None:
|
370
|
+
new_path = ensure_uri_compnents(new_path)
|
371
|
+
logger.info(f"Update path of {path} to {new_path}")
|
372
|
+
assert new_path.endswith("/"), "New path must end with /"
|
373
|
+
if new_path.startswith("/"):
|
374
|
+
new_path = new_path[1:]
|
375
|
+
|
376
|
+
# check if new path is under the user's directory
|
377
|
+
first_component = new_path.split("/")[0]
|
378
|
+
if not (first_component == user.username or user.is_admin):
|
379
|
+
raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
|
380
|
+
elif user.is_admin:
|
381
|
+
_is_user = await conn.user.get_user(first_component)
|
382
|
+
if not _is_user:
|
383
|
+
raise HTTPException(status_code=404, detail="User not found, path must start with username")
|
384
|
+
|
385
|
+
# check if old path is under the user's directory (non-admin)
|
386
|
+
if not path.startswith(f"{user.username}/") and not user.is_admin:
|
387
|
+
raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
|
388
|
+
# currently only move own file, with overwrite
|
389
|
+
await conn.move_path(path, new_path, user_id = user.id)
|
358
390
|
|
359
391
|
return Response(status_code=200, content="OK")
|
360
392
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|