rclone-api 1.0.49__py2.py3-none-any.whl → 1.0.50__py2.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.
- rclone_api/file.py +54 -47
- rclone_api/group_files.py +29 -8
- rclone_api/rclone.py +670 -677
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/METADATA +1 -1
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/RECORD +9 -9
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/LICENSE +0 -0
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/WHEEL +0 -0
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.0.49.dist-info → rclone_api-1.0.50.dist-info}/top_level.txt +0 -0
rclone_api/rclone.py
CHANGED
@@ -1,677 +1,670 @@
|
|
1
|
-
"""
|
2
|
-
Unit test file.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import os
|
6
|
-
import subprocess
|
7
|
-
import time
|
8
|
-
import warnings
|
9
|
-
from concurrent.futures import Future, ThreadPoolExecutor
|
10
|
-
from enum import Enum
|
11
|
-
from fnmatch import fnmatch
|
12
|
-
from pathlib import Path
|
13
|
-
from tempfile import TemporaryDirectory
|
14
|
-
from typing import Generator
|
15
|
-
|
16
|
-
from rclone_api import Dir
|
17
|
-
from rclone_api.completed_process import CompletedProcess
|
18
|
-
from rclone_api.config import Config
|
19
|
-
from rclone_api.convert import convert_to_filestr_list, convert_to_str
|
20
|
-
from rclone_api.deprecated import deprecated
|
21
|
-
from rclone_api.diff import DiffItem, diff_stream_from_running_process
|
22
|
-
from rclone_api.dir_listing import DirListing
|
23
|
-
from rclone_api.exec import RcloneExec
|
24
|
-
from rclone_api.file import File
|
25
|
-
from rclone_api.group_files import group_files
|
26
|
-
from rclone_api.process import Process
|
27
|
-
from rclone_api.remote import Remote
|
28
|
-
from rclone_api.rpath import RPath
|
29
|
-
from rclone_api.util import (
|
30
|
-
get_rclone_exe,
|
31
|
-
get_verbose,
|
32
|
-
to_path,
|
33
|
-
wait_for_mount,
|
34
|
-
)
|
35
|
-
from rclone_api.walk import walk
|
36
|
-
|
37
|
-
EXECUTOR = ThreadPoolExecutor(16)
|
38
|
-
|
39
|
-
|
40
|
-
def rclone_verbose(verbose: bool | None) -> bool:
|
41
|
-
if verbose is not None:
|
42
|
-
os.environ["RCLONE_API_VERBOSE"] = "1" if verbose else "0"
|
43
|
-
return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
|
44
|
-
|
45
|
-
|
46
|
-
class ModTimeStrategy(Enum):
|
47
|
-
USE_SERVER_MODTIME = "use-server-modtime"
|
48
|
-
NO_MODTIME = "no-modtime"
|
49
|
-
|
50
|
-
|
51
|
-
class Rclone:
|
52
|
-
def __init__(
|
53
|
-
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
54
|
-
) -> None:
|
55
|
-
if isinstance(rclone_conf, Path):
|
56
|
-
if not rclone_conf.exists():
|
57
|
-
raise ValueError(f"Rclone config file not found: {rclone_conf}")
|
58
|
-
self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
|
59
|
-
|
60
|
-
def _run(
|
61
|
-
self, cmd: list[str], check: bool = True, capture: bool | None = None
|
62
|
-
) -> subprocess.CompletedProcess:
|
63
|
-
return self._exec.execute(cmd, check=check, capture=capture)
|
64
|
-
|
65
|
-
def _launch_process(self, cmd: list[str], capture: bool | None = None) -> Process:
|
66
|
-
return self._exec.launch_process(cmd, capture=capture)
|
67
|
-
|
68
|
-
def webgui(self, other_args: list[str] | None = None) -> Process:
|
69
|
-
"""Launch the Rclone web GUI."""
|
70
|
-
cmd = ["rcd", "--rc-web-gui"]
|
71
|
-
if other_args:
|
72
|
-
cmd += other_args
|
73
|
-
return self._launch_process(cmd, capture=False)
|
74
|
-
|
75
|
-
def launch_server(
|
76
|
-
self,
|
77
|
-
addr: str | None = None,
|
78
|
-
user: str | None = None,
|
79
|
-
password: str | None = None,
|
80
|
-
other_args: list[str] | None = None,
|
81
|
-
) -> Process:
|
82
|
-
"""Launch the Rclone server so it can receive commands"""
|
83
|
-
cmd = ["rcd"]
|
84
|
-
if addr is not None:
|
85
|
-
cmd += ["--rc-addr", addr]
|
86
|
-
if user is not None:
|
87
|
-
cmd += ["--rc-user", user]
|
88
|
-
if password is not None:
|
89
|
-
cmd += ["--rc-pass", password]
|
90
|
-
if other_args:
|
91
|
-
cmd += other_args
|
92
|
-
out = self._launch_process(cmd, capture=False)
|
93
|
-
time.sleep(1) # Give it some time to launch
|
94
|
-
return out
|
95
|
-
|
96
|
-
def remote_control(
|
97
|
-
self,
|
98
|
-
addr: str,
|
99
|
-
user: str | None = None,
|
100
|
-
password: str | None = None,
|
101
|
-
capture: bool | None = None,
|
102
|
-
other_args: list[str] | None = None,
|
103
|
-
) -> CompletedProcess:
|
104
|
-
cmd = ["rc"]
|
105
|
-
if addr:
|
106
|
-
cmd += ["--rc-addr", addr]
|
107
|
-
if user is not None:
|
108
|
-
cmd += ["--rc-user", user]
|
109
|
-
if password is not None:
|
110
|
-
cmd += ["--rc-pass", password]
|
111
|
-
if other_args:
|
112
|
-
cmd += other_args
|
113
|
-
cp = self._run(cmd, capture=capture)
|
114
|
-
return CompletedProcess.from_subprocess(cp)
|
115
|
-
|
116
|
-
def obscure(self, password: str) -> str:
|
117
|
-
"""Obscure a password for use in rclone config files."""
|
118
|
-
cmd_list: list[str] = ["obscure", password]
|
119
|
-
cp = self._run(cmd_list)
|
120
|
-
return cp.stdout.strip()
|
121
|
-
|
122
|
-
def ls(
|
123
|
-
self,
|
124
|
-
path: Dir | Remote | str,
|
125
|
-
max_depth: int | None = None,
|
126
|
-
glob: str | None = None,
|
127
|
-
) -> DirListing:
|
128
|
-
"""List files in the given path.
|
129
|
-
|
130
|
-
Args:
|
131
|
-
path: Remote path or Remote object to list
|
132
|
-
max_depth: Maximum recursion depth (0 means no recursion)
|
133
|
-
|
134
|
-
Returns:
|
135
|
-
List of File objects found at the path
|
136
|
-
"""
|
137
|
-
|
138
|
-
if isinstance(path, str):
|
139
|
-
path = Dir(
|
140
|
-
to_path(path, self)
|
141
|
-
) # assume it's a directory if ls is being called.
|
142
|
-
|
143
|
-
cmd = ["lsjson"]
|
144
|
-
if max_depth is not None:
|
145
|
-
if max_depth < 0:
|
146
|
-
cmd.append("--recursive")
|
147
|
-
if max_depth > 0:
|
148
|
-
cmd.append("--max-depth")
|
149
|
-
cmd.append(str(max_depth))
|
150
|
-
cmd.append(str(path))
|
151
|
-
remote = path.remote if isinstance(path, Dir) else path
|
152
|
-
assert isinstance(remote, Remote)
|
153
|
-
|
154
|
-
cp = self._run(cmd, check=True)
|
155
|
-
text = cp.stdout
|
156
|
-
parent_path: str | None = None
|
157
|
-
if isinstance(path, Dir):
|
158
|
-
parent_path = path.path.path
|
159
|
-
paths: list[RPath] = RPath.from_json_str(text, remote, parent_path=parent_path)
|
160
|
-
# print(parent_path)
|
161
|
-
for o in paths:
|
162
|
-
o.set_rclone(self)
|
163
|
-
|
164
|
-
# do we have a glob pattern?
|
165
|
-
if glob is not None:
|
166
|
-
paths = [p for p in paths if fnmatch(p.path, glob)]
|
167
|
-
return DirListing(paths)
|
168
|
-
|
169
|
-
def listremotes(self) -> list[Remote]:
|
170
|
-
cmd = ["listremotes"]
|
171
|
-
cp = self._run(cmd)
|
172
|
-
text: str = cp.stdout
|
173
|
-
tmp = text.splitlines()
|
174
|
-
tmp = [t.strip() for t in tmp]
|
175
|
-
# strip out ":" from the end
|
176
|
-
tmp = [t.replace(":", "") for t in tmp]
|
177
|
-
out = [Remote(name=t, rclone=self) for t in tmp]
|
178
|
-
return out
|
179
|
-
|
180
|
-
def diff(self, src: str, dst: str) -> Generator[DiffItem, None, None]:
|
181
|
-
"""Be extra careful with the src and dst values. If you are off by one
|
182
|
-
parent directory, you will get a huge amount of false diffs."""
|
183
|
-
cmd = [
|
184
|
-
"check",
|
185
|
-
src,
|
186
|
-
dst,
|
187
|
-
"--checkers",
|
188
|
-
"1000",
|
189
|
-
"--log-level",
|
190
|
-
"INFO",
|
191
|
-
"--combined",
|
192
|
-
"-",
|
193
|
-
]
|
194
|
-
proc = self._launch_process(cmd, capture=True)
|
195
|
-
item: DiffItem
|
196
|
-
for item in diff_stream_from_running_process(proc, src_slug=src, dst_slug=dst):
|
197
|
-
if item is None:
|
198
|
-
break
|
199
|
-
yield item
|
200
|
-
|
201
|
-
def walk(
|
202
|
-
self, path: Dir | Remote | str, max_depth: int = -1, breadth_first: bool = True
|
203
|
-
) -> Generator[DirListing, None, None]:
|
204
|
-
"""Walk through the given path recursively.
|
205
|
-
|
206
|
-
Args:
|
207
|
-
path: Remote path or Remote object to walk through
|
208
|
-
max_depth: Maximum depth to traverse (-1 for unlimited)
|
209
|
-
|
210
|
-
Yields:
|
211
|
-
DirListing: Directory listing for each directory encountered
|
212
|
-
"""
|
213
|
-
dir_obj: Dir
|
214
|
-
if isinstance(path, Dir):
|
215
|
-
# Create a Remote object for the path
|
216
|
-
remote = path.remote
|
217
|
-
rpath = RPath(
|
218
|
-
remote=remote,
|
219
|
-
path=path.path.path,
|
220
|
-
name=path.path.name,
|
221
|
-
size=0,
|
222
|
-
mime_type="inode/directory",
|
223
|
-
mod_time="",
|
224
|
-
is_dir=True,
|
225
|
-
)
|
226
|
-
rpath.set_rclone(self)
|
227
|
-
dir_obj = Dir(rpath)
|
228
|
-
elif isinstance(path, str):
|
229
|
-
dir_obj = Dir(to_path(path, self))
|
230
|
-
elif isinstance(path, Remote):
|
231
|
-
dir_obj = Dir(path)
|
232
|
-
else:
|
233
|
-
dir_obj = Dir(path) # shut up pyright
|
234
|
-
assert f"Invalid type for path: {type(path)}"
|
235
|
-
|
236
|
-
yield from walk(dir_obj, max_depth=max_depth, breadth_first=breadth_first)
|
237
|
-
|
238
|
-
def cleanup(
|
239
|
-
self, path: str, other_args: list[str] | None = None
|
240
|
-
) -> CompletedProcess:
|
241
|
-
"""Cleanup any resources used by the Rclone instance."""
|
242
|
-
# rclone cleanup remote:path [flags]
|
243
|
-
cmd = ["cleanup", path]
|
244
|
-
if other_args:
|
245
|
-
cmd += other_args
|
246
|
-
out = self._run(cmd)
|
247
|
-
return CompletedProcess.from_subprocess(out)
|
248
|
-
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
check=
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
#
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
"
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
"
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
return
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
if
|
525
|
-
cmd_list
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
if
|
609
|
-
other_cmds.append(
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
other_cmds.append(
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
other_cmds.append(
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
other_cmds.append(
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
if allow_other:
|
672
|
-
cmd_list.append("--allow-other")
|
673
|
-
proc = self._launch_process(cmd_list)
|
674
|
-
time.sleep(2) # give it a moment to start
|
675
|
-
if proc.poll() is not None:
|
676
|
-
raise ValueError("NFS serve process failed to start")
|
677
|
-
return proc
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import subprocess
|
7
|
+
import time
|
8
|
+
import warnings
|
9
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
10
|
+
from enum import Enum
|
11
|
+
from fnmatch import fnmatch
|
12
|
+
from pathlib import Path
|
13
|
+
from tempfile import TemporaryDirectory
|
14
|
+
from typing import Generator
|
15
|
+
|
16
|
+
from rclone_api import Dir
|
17
|
+
from rclone_api.completed_process import CompletedProcess
|
18
|
+
from rclone_api.config import Config
|
19
|
+
from rclone_api.convert import convert_to_filestr_list, convert_to_str
|
20
|
+
from rclone_api.deprecated import deprecated
|
21
|
+
from rclone_api.diff import DiffItem, diff_stream_from_running_process
|
22
|
+
from rclone_api.dir_listing import DirListing
|
23
|
+
from rclone_api.exec import RcloneExec
|
24
|
+
from rclone_api.file import File
|
25
|
+
from rclone_api.group_files import group_files
|
26
|
+
from rclone_api.process import Process
|
27
|
+
from rclone_api.remote import Remote
|
28
|
+
from rclone_api.rpath import RPath
|
29
|
+
from rclone_api.util import (
|
30
|
+
get_rclone_exe,
|
31
|
+
get_verbose,
|
32
|
+
to_path,
|
33
|
+
wait_for_mount,
|
34
|
+
)
|
35
|
+
from rclone_api.walk import walk
|
36
|
+
|
37
|
+
EXECUTOR = ThreadPoolExecutor(16)
|
38
|
+
|
39
|
+
|
40
|
+
def rclone_verbose(verbose: bool | None) -> bool:
|
41
|
+
if verbose is not None:
|
42
|
+
os.environ["RCLONE_API_VERBOSE"] = "1" if verbose else "0"
|
43
|
+
return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
|
44
|
+
|
45
|
+
|
46
|
+
class ModTimeStrategy(Enum):
|
47
|
+
USE_SERVER_MODTIME = "use-server-modtime"
|
48
|
+
NO_MODTIME = "no-modtime"
|
49
|
+
|
50
|
+
|
51
|
+
class Rclone:
|
52
|
+
def __init__(
|
53
|
+
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
54
|
+
) -> None:
|
55
|
+
if isinstance(rclone_conf, Path):
|
56
|
+
if not rclone_conf.exists():
|
57
|
+
raise ValueError(f"Rclone config file not found: {rclone_conf}")
|
58
|
+
self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
|
59
|
+
|
60
|
+
def _run(
|
61
|
+
self, cmd: list[str], check: bool = True, capture: bool | None = None
|
62
|
+
) -> subprocess.CompletedProcess:
|
63
|
+
return self._exec.execute(cmd, check=check, capture=capture)
|
64
|
+
|
65
|
+
def _launch_process(self, cmd: list[str], capture: bool | None = None) -> Process:
|
66
|
+
return self._exec.launch_process(cmd, capture=capture)
|
67
|
+
|
68
|
+
def webgui(self, other_args: list[str] | None = None) -> Process:
|
69
|
+
"""Launch the Rclone web GUI."""
|
70
|
+
cmd = ["rcd", "--rc-web-gui"]
|
71
|
+
if other_args:
|
72
|
+
cmd += other_args
|
73
|
+
return self._launch_process(cmd, capture=False)
|
74
|
+
|
75
|
+
def launch_server(
|
76
|
+
self,
|
77
|
+
addr: str | None = None,
|
78
|
+
user: str | None = None,
|
79
|
+
password: str | None = None,
|
80
|
+
other_args: list[str] | None = None,
|
81
|
+
) -> Process:
|
82
|
+
"""Launch the Rclone server so it can receive commands"""
|
83
|
+
cmd = ["rcd"]
|
84
|
+
if addr is not None:
|
85
|
+
cmd += ["--rc-addr", addr]
|
86
|
+
if user is not None:
|
87
|
+
cmd += ["--rc-user", user]
|
88
|
+
if password is not None:
|
89
|
+
cmd += ["--rc-pass", password]
|
90
|
+
if other_args:
|
91
|
+
cmd += other_args
|
92
|
+
out = self._launch_process(cmd, capture=False)
|
93
|
+
time.sleep(1) # Give it some time to launch
|
94
|
+
return out
|
95
|
+
|
96
|
+
def remote_control(
|
97
|
+
self,
|
98
|
+
addr: str,
|
99
|
+
user: str | None = None,
|
100
|
+
password: str | None = None,
|
101
|
+
capture: bool | None = None,
|
102
|
+
other_args: list[str] | None = None,
|
103
|
+
) -> CompletedProcess:
|
104
|
+
cmd = ["rc"]
|
105
|
+
if addr:
|
106
|
+
cmd += ["--rc-addr", addr]
|
107
|
+
if user is not None:
|
108
|
+
cmd += ["--rc-user", user]
|
109
|
+
if password is not None:
|
110
|
+
cmd += ["--rc-pass", password]
|
111
|
+
if other_args:
|
112
|
+
cmd += other_args
|
113
|
+
cp = self._run(cmd, capture=capture)
|
114
|
+
return CompletedProcess.from_subprocess(cp)
|
115
|
+
|
116
|
+
def obscure(self, password: str) -> str:
|
117
|
+
"""Obscure a password for use in rclone config files."""
|
118
|
+
cmd_list: list[str] = ["obscure", password]
|
119
|
+
cp = self._run(cmd_list)
|
120
|
+
return cp.stdout.strip()
|
121
|
+
|
122
|
+
def ls(
|
123
|
+
self,
|
124
|
+
path: Dir | Remote | str,
|
125
|
+
max_depth: int | None = None,
|
126
|
+
glob: str | None = None,
|
127
|
+
) -> DirListing:
|
128
|
+
"""List files in the given path.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
path: Remote path or Remote object to list
|
132
|
+
max_depth: Maximum recursion depth (0 means no recursion)
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
List of File objects found at the path
|
136
|
+
"""
|
137
|
+
|
138
|
+
if isinstance(path, str):
|
139
|
+
path = Dir(
|
140
|
+
to_path(path, self)
|
141
|
+
) # assume it's a directory if ls is being called.
|
142
|
+
|
143
|
+
cmd = ["lsjson"]
|
144
|
+
if max_depth is not None:
|
145
|
+
if max_depth < 0:
|
146
|
+
cmd.append("--recursive")
|
147
|
+
if max_depth > 0:
|
148
|
+
cmd.append("--max-depth")
|
149
|
+
cmd.append(str(max_depth))
|
150
|
+
cmd.append(str(path))
|
151
|
+
remote = path.remote if isinstance(path, Dir) else path
|
152
|
+
assert isinstance(remote, Remote)
|
153
|
+
|
154
|
+
cp = self._run(cmd, check=True)
|
155
|
+
text = cp.stdout
|
156
|
+
parent_path: str | None = None
|
157
|
+
if isinstance(path, Dir):
|
158
|
+
parent_path = path.path.path
|
159
|
+
paths: list[RPath] = RPath.from_json_str(text, remote, parent_path=parent_path)
|
160
|
+
# print(parent_path)
|
161
|
+
for o in paths:
|
162
|
+
o.set_rclone(self)
|
163
|
+
|
164
|
+
# do we have a glob pattern?
|
165
|
+
if glob is not None:
|
166
|
+
paths = [p for p in paths if fnmatch(p.path, glob)]
|
167
|
+
return DirListing(paths)
|
168
|
+
|
169
|
+
def listremotes(self) -> list[Remote]:
|
170
|
+
cmd = ["listremotes"]
|
171
|
+
cp = self._run(cmd)
|
172
|
+
text: str = cp.stdout
|
173
|
+
tmp = text.splitlines()
|
174
|
+
tmp = [t.strip() for t in tmp]
|
175
|
+
# strip out ":" from the end
|
176
|
+
tmp = [t.replace(":", "") for t in tmp]
|
177
|
+
out = [Remote(name=t, rclone=self) for t in tmp]
|
178
|
+
return out
|
179
|
+
|
180
|
+
def diff(self, src: str, dst: str) -> Generator[DiffItem, None, None]:
|
181
|
+
"""Be extra careful with the src and dst values. If you are off by one
|
182
|
+
parent directory, you will get a huge amount of false diffs."""
|
183
|
+
cmd = [
|
184
|
+
"check",
|
185
|
+
src,
|
186
|
+
dst,
|
187
|
+
"--checkers",
|
188
|
+
"1000",
|
189
|
+
"--log-level",
|
190
|
+
"INFO",
|
191
|
+
"--combined",
|
192
|
+
"-",
|
193
|
+
]
|
194
|
+
proc = self._launch_process(cmd, capture=True)
|
195
|
+
item: DiffItem
|
196
|
+
for item in diff_stream_from_running_process(proc, src_slug=src, dst_slug=dst):
|
197
|
+
if item is None:
|
198
|
+
break
|
199
|
+
yield item
|
200
|
+
|
201
|
+
def walk(
|
202
|
+
self, path: Dir | Remote | str, max_depth: int = -1, breadth_first: bool = True
|
203
|
+
) -> Generator[DirListing, None, None]:
|
204
|
+
"""Walk through the given path recursively.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
path: Remote path or Remote object to walk through
|
208
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
209
|
+
|
210
|
+
Yields:
|
211
|
+
DirListing: Directory listing for each directory encountered
|
212
|
+
"""
|
213
|
+
dir_obj: Dir
|
214
|
+
if isinstance(path, Dir):
|
215
|
+
# Create a Remote object for the path
|
216
|
+
remote = path.remote
|
217
|
+
rpath = RPath(
|
218
|
+
remote=remote,
|
219
|
+
path=path.path.path,
|
220
|
+
name=path.path.name,
|
221
|
+
size=0,
|
222
|
+
mime_type="inode/directory",
|
223
|
+
mod_time="",
|
224
|
+
is_dir=True,
|
225
|
+
)
|
226
|
+
rpath.set_rclone(self)
|
227
|
+
dir_obj = Dir(rpath)
|
228
|
+
elif isinstance(path, str):
|
229
|
+
dir_obj = Dir(to_path(path, self))
|
230
|
+
elif isinstance(path, Remote):
|
231
|
+
dir_obj = Dir(path)
|
232
|
+
else:
|
233
|
+
dir_obj = Dir(path) # shut up pyright
|
234
|
+
assert f"Invalid type for path: {type(path)}"
|
235
|
+
|
236
|
+
yield from walk(dir_obj, max_depth=max_depth, breadth_first=breadth_first)
|
237
|
+
|
238
|
+
def cleanup(
|
239
|
+
self, path: str, other_args: list[str] | None = None
|
240
|
+
) -> CompletedProcess:
|
241
|
+
"""Cleanup any resources used by the Rclone instance."""
|
242
|
+
# rclone cleanup remote:path [flags]
|
243
|
+
cmd = ["cleanup", path]
|
244
|
+
if other_args:
|
245
|
+
cmd += other_args
|
246
|
+
out = self._run(cmd)
|
247
|
+
return CompletedProcess.from_subprocess(out)
|
248
|
+
|
249
|
+
def copy_to(
|
250
|
+
self,
|
251
|
+
src: File | str,
|
252
|
+
dst: File | str,
|
253
|
+
check=True,
|
254
|
+
other_args: list[str] | None = None,
|
255
|
+
) -> None:
|
256
|
+
"""Copy multiple files from source to destination.
|
257
|
+
|
258
|
+
Warning - slow.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
payload: Dictionary of source and destination file paths
|
262
|
+
"""
|
263
|
+
src = src if isinstance(src, str) else str(src.path)
|
264
|
+
dst = dst if isinstance(dst, str) else str(dst.path)
|
265
|
+
cmd_list: list[str] = ["copyto", src, dst]
|
266
|
+
if other_args is not None:
|
267
|
+
cmd_list += other_args
|
268
|
+
self._run(cmd_list, check=check)
|
269
|
+
|
270
|
+
def copy_files(
|
271
|
+
self,
|
272
|
+
src: str,
|
273
|
+
dst: str,
|
274
|
+
files: list[str],
|
275
|
+
check=True,
|
276
|
+
other_args: list[str] | None = None,
|
277
|
+
) -> list[CompletedProcess]:
|
278
|
+
"""Copy multiple files from source to destination.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
payload: Dictionary of source and destination file paths
|
282
|
+
"""
|
283
|
+
payload: list[str] = convert_to_filestr_list(files)
|
284
|
+
if len(payload) == 0:
|
285
|
+
return []
|
286
|
+
|
287
|
+
datalists: dict[str, list[str]] = group_files(payload, fully_qualified=False)
|
288
|
+
# out: subprocess.CompletedProcess | None = None
|
289
|
+
out: list[CompletedProcess] = []
|
290
|
+
|
291
|
+
futures: list[Future] = []
|
292
|
+
|
293
|
+
for common_prefix, files in datalists.items():
|
294
|
+
|
295
|
+
def _task(files=files) -> subprocess.CompletedProcess:
|
296
|
+
with TemporaryDirectory() as tmpdir:
|
297
|
+
include_files_txt = Path(tmpdir) / "include_files.txt"
|
298
|
+
include_files_txt.write_text("\n".join(files), encoding="utf-8")
|
299
|
+
if common_prefix:
|
300
|
+
src_path = f"{src}/{common_prefix}"
|
301
|
+
dst_path = f"{dst}/{common_prefix}"
|
302
|
+
else:
|
303
|
+
src_path = src
|
304
|
+
dst_path = dst
|
305
|
+
# print(include_files_txt)
|
306
|
+
cmd_list: list[str] = [
|
307
|
+
"copy",
|
308
|
+
src_path,
|
309
|
+
dst_path,
|
310
|
+
"--files-from",
|
311
|
+
str(include_files_txt),
|
312
|
+
"--checkers",
|
313
|
+
"1000",
|
314
|
+
"--transfers",
|
315
|
+
"1000",
|
316
|
+
]
|
317
|
+
if other_args is not None:
|
318
|
+
cmd_list += other_args
|
319
|
+
out = self._run(cmd_list)
|
320
|
+
return out
|
321
|
+
|
322
|
+
fut: Future = EXECUTOR.submit(_task)
|
323
|
+
futures.append(fut)
|
324
|
+
for fut in futures:
|
325
|
+
cp: subprocess.CompletedProcess = fut.result()
|
326
|
+
assert cp is not None
|
327
|
+
out.append(CompletedProcess.from_subprocess(cp))
|
328
|
+
if cp.returncode != 0:
|
329
|
+
if check:
|
330
|
+
raise ValueError(f"Error deleting files: {cp.stderr}")
|
331
|
+
else:
|
332
|
+
warnings.warn(f"Error deleting files: {cp.stderr}")
|
333
|
+
return out
|
334
|
+
|
335
|
+
def copy(self, src: Dir | str, dst: Dir | str) -> CompletedProcess:
|
336
|
+
"""Copy files from source to destination.
|
337
|
+
|
338
|
+
Args:
|
339
|
+
src: Source directory
|
340
|
+
dst: Destination directory
|
341
|
+
"""
|
342
|
+
# src_dir = src.path.path
|
343
|
+
# dst_dir = dst.path.path
|
344
|
+
src_dir = convert_to_str(src)
|
345
|
+
dst_dir = convert_to_str(dst)
|
346
|
+
cmd_list: list[str] = ["copy", src_dir, dst_dir]
|
347
|
+
cp = self._run(cmd_list)
|
348
|
+
return CompletedProcess.from_subprocess(cp)
|
349
|
+
|
350
|
+
def purge(self, path: Dir | str) -> CompletedProcess:
|
351
|
+
"""Purge a directory"""
|
352
|
+
# path should always be a string
|
353
|
+
path = path if isinstance(path, str) else str(path.path)
|
354
|
+
cmd_list: list[str] = ["purge", str(path)]
|
355
|
+
cp = self._run(cmd_list)
|
356
|
+
return CompletedProcess.from_subprocess(cp)
|
357
|
+
|
358
|
+
def delete_files(
|
359
|
+
self,
|
360
|
+
files: str | File | list[str] | list[File],
|
361
|
+
check=True,
|
362
|
+
rmdirs=False,
|
363
|
+
verbose: bool | None = None,
|
364
|
+
other_args: list[str] | None = None,
|
365
|
+
) -> CompletedProcess:
|
366
|
+
"""Delete a directory"""
|
367
|
+
payload: list[str] = convert_to_filestr_list(files)
|
368
|
+
if len(payload) == 0:
|
369
|
+
cp = subprocess.CompletedProcess(
|
370
|
+
args=["rclone", "delete", "--files-from", "[]"],
|
371
|
+
returncode=0,
|
372
|
+
stdout="",
|
373
|
+
stderr="",
|
374
|
+
)
|
375
|
+
return CompletedProcess.from_subprocess(cp)
|
376
|
+
|
377
|
+
datalists: dict[str, list[str]] = group_files(payload)
|
378
|
+
completed_processes: list[subprocess.CompletedProcess] = []
|
379
|
+
verbose = get_verbose(verbose)
|
380
|
+
|
381
|
+
futures: list[Future] = []
|
382
|
+
|
383
|
+
for remote, files in datalists.items():
|
384
|
+
|
385
|
+
def _task(
|
386
|
+
files=files, check=check, remote=remote
|
387
|
+
) -> subprocess.CompletedProcess:
|
388
|
+
with TemporaryDirectory() as tmpdir:
|
389
|
+
include_files_txt = Path(tmpdir) / "include_files.txt"
|
390
|
+
include_files_txt.write_text("\n".join(files), encoding="utf-8")
|
391
|
+
|
392
|
+
# print(include_files_txt)
|
393
|
+
cmd_list: list[str] = [
|
394
|
+
"delete",
|
395
|
+
remote,
|
396
|
+
"--files-from",
|
397
|
+
str(include_files_txt),
|
398
|
+
"--checkers",
|
399
|
+
"1000",
|
400
|
+
"--transfers",
|
401
|
+
"1000",
|
402
|
+
]
|
403
|
+
if verbose:
|
404
|
+
cmd_list.append("-vvvv")
|
405
|
+
if rmdirs:
|
406
|
+
cmd_list.append("--rmdirs")
|
407
|
+
if other_args:
|
408
|
+
cmd_list += other_args
|
409
|
+
out = self._run(cmd_list, check=check)
|
410
|
+
if out.returncode != 0:
|
411
|
+
if check:
|
412
|
+
completed_processes.append(out)
|
413
|
+
raise ValueError(f"Error deleting files: {out}")
|
414
|
+
else:
|
415
|
+
warnings.warn(f"Error deleting files: {out}")
|
416
|
+
return out
|
417
|
+
|
418
|
+
fut: Future = EXECUTOR.submit(_task)
|
419
|
+
futures.append(fut)
|
420
|
+
|
421
|
+
for fut in futures:
|
422
|
+
out = fut.result()
|
423
|
+
assert out is not None
|
424
|
+
completed_processes.append(out)
|
425
|
+
|
426
|
+
return CompletedProcess(completed_processes)
|
427
|
+
|
428
|
+
@deprecated("delete_files")
|
429
|
+
def deletefiles(
|
430
|
+
self, files: str | File | list[str] | list[File]
|
431
|
+
) -> CompletedProcess:
|
432
|
+
out = self.delete_files(files)
|
433
|
+
return out
|
434
|
+
|
435
|
+
def exists(self, path: Dir | Remote | str | File) -> bool:
|
436
|
+
"""Check if a file or directory exists."""
|
437
|
+
arg: str = convert_to_str(path)
|
438
|
+
assert isinstance(arg, str)
|
439
|
+
try:
|
440
|
+
dir_listing = self.ls(arg)
|
441
|
+
# print(dir_listing)
|
442
|
+
return len(dir_listing.dirs) > 0 or len(dir_listing.files) > 0
|
443
|
+
except subprocess.CalledProcessError:
|
444
|
+
return False
|
445
|
+
|
446
|
+
def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
|
447
|
+
"""Check if two directories are in sync."""
|
448
|
+
src = convert_to_str(src)
|
449
|
+
dst = convert_to_str(dst)
|
450
|
+
cmd_list: list[str] = ["check", str(src), str(dst)]
|
451
|
+
try:
|
452
|
+
self._run(cmd_list)
|
453
|
+
return True
|
454
|
+
except subprocess.CalledProcessError:
|
455
|
+
return False
|
456
|
+
|
457
|
+
def copy_dir(
|
458
|
+
self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
|
459
|
+
) -> CompletedProcess:
|
460
|
+
"""Copy a directory from source to destination."""
|
461
|
+
# convert src to str, also dst
|
462
|
+
src = convert_to_str(src)
|
463
|
+
dst = convert_to_str(dst)
|
464
|
+
cmd_list: list[str] = ["copy", src, dst]
|
465
|
+
if args is not None:
|
466
|
+
cmd_list += args
|
467
|
+
cp = self._run(cmd_list)
|
468
|
+
return CompletedProcess.from_subprocess(cp)
|
469
|
+
|
470
|
+
def copy_remote(
|
471
|
+
self, src: Remote, dst: Remote, args: list[str] | None = None
|
472
|
+
) -> CompletedProcess:
|
473
|
+
"""Copy a remote to another remote."""
|
474
|
+
cmd_list: list[str] = ["copy", str(src), str(dst)]
|
475
|
+
if args is not None:
|
476
|
+
cmd_list += args
|
477
|
+
# return self._run(cmd_list)
|
478
|
+
cp = self._run(cmd_list)
|
479
|
+
return CompletedProcess.from_subprocess(cp)
|
480
|
+
|
481
|
+
def mount(
|
482
|
+
self,
|
483
|
+
src: Remote | Dir | str,
|
484
|
+
outdir: Path,
|
485
|
+
allow_writes=False,
|
486
|
+
use_links=True,
|
487
|
+
vfs_cache_mode="full",
|
488
|
+
other_cmds: list[str] | None = None,
|
489
|
+
) -> Process:
|
490
|
+
"""Mount a remote or directory to a local path.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
src: Remote or directory to mount
|
494
|
+
outdir: Local path to mount to
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
CompletedProcess from the mount command execution
|
498
|
+
|
499
|
+
Raises:
|
500
|
+
subprocess.CalledProcessError: If the mount operation fails
|
501
|
+
"""
|
502
|
+
if outdir.exists():
|
503
|
+
is_empty = not list(outdir.iterdir())
|
504
|
+
if not is_empty:
|
505
|
+
raise ValueError(
|
506
|
+
f"Mount directory already exists and is not empty: {outdir}"
|
507
|
+
)
|
508
|
+
outdir.rmdir()
|
509
|
+
try:
|
510
|
+
outdir.parent.mkdir(parents=True, exist_ok=True)
|
511
|
+
except PermissionError:
|
512
|
+
warnings.warn(
|
513
|
+
f"Permission error creating parent directory: {outdir.parent}"
|
514
|
+
)
|
515
|
+
src_str = convert_to_str(src)
|
516
|
+
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
517
|
+
if not allow_writes:
|
518
|
+
cmd_list.append("--read-only")
|
519
|
+
if use_links:
|
520
|
+
cmd_list.append("--links")
|
521
|
+
if vfs_cache_mode:
|
522
|
+
cmd_list.append("--vfs-cache-mode")
|
523
|
+
cmd_list.append(vfs_cache_mode)
|
524
|
+
if other_cmds:
|
525
|
+
cmd_list += other_cmds
|
526
|
+
proc = self._launch_process(cmd_list)
|
527
|
+
wait_for_mount(outdir, proc)
|
528
|
+
return proc
|
529
|
+
|
530
|
+
def mount_webdav(
|
531
|
+
self,
|
532
|
+
url: str,
|
533
|
+
outdir: Path,
|
534
|
+
vfs_cache_mode="full",
|
535
|
+
vfs_disk_space_total_size: str | None = "10G",
|
536
|
+
other_cmds: list[str] | None = None,
|
537
|
+
) -> Process:
|
538
|
+
"""Mount a remote or directory to a local path.
|
539
|
+
|
540
|
+
Args:
|
541
|
+
src: Remote or directory to mount
|
542
|
+
outdir: Local path to mount to
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
CompletedProcess from the mount command execution
|
546
|
+
|
547
|
+
Raises:
|
548
|
+
subprocess.CalledProcessError: If the mount operation fails
|
549
|
+
"""
|
550
|
+
if outdir.exists():
|
551
|
+
is_empty = not list(outdir.iterdir())
|
552
|
+
if not is_empty:
|
553
|
+
raise ValueError(
|
554
|
+
f"Mount directory already exists and is not empty: {outdir}"
|
555
|
+
)
|
556
|
+
outdir.rmdir()
|
557
|
+
|
558
|
+
src_str = url
|
559
|
+
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
560
|
+
cmd_list.append("--vfs-cache-mode")
|
561
|
+
cmd_list.append(vfs_cache_mode)
|
562
|
+
if other_cmds:
|
563
|
+
cmd_list += other_cmds
|
564
|
+
if vfs_disk_space_total_size is not None:
|
565
|
+
cmd_list.append("--vfs-cache-max-size")
|
566
|
+
cmd_list.append(vfs_disk_space_total_size)
|
567
|
+
proc = self._launch_process(cmd_list)
|
568
|
+
wait_for_mount(outdir, proc)
|
569
|
+
return proc
|
570
|
+
|
571
|
+
def mount_s3(
|
572
|
+
self,
|
573
|
+
url: str,
|
574
|
+
outdir: Path,
|
575
|
+
allow_writes=False,
|
576
|
+
vfs_cache_mode="full",
|
577
|
+
# dir-cache-time
|
578
|
+
dir_cache_time: str | None = "1h",
|
579
|
+
attribute_timeout: str | None = "1h",
|
580
|
+
# --vfs-cache-max-size
|
581
|
+
# vfs-cache-max-size
|
582
|
+
vfs_disk_space_total_size: str | None = "100M",
|
583
|
+
transfers: int | None = 128,
|
584
|
+
modtime_strategy: (
|
585
|
+
ModTimeStrategy | None
|
586
|
+
) = ModTimeStrategy.USE_SERVER_MODTIME, # speeds up S3 operations
|
587
|
+
vfs_read_chunk_streams: int | None = 16,
|
588
|
+
vfs_read_chunk_size: str | None = "4M",
|
589
|
+
vfs_fast_fingerprint: bool = True,
|
590
|
+
# vfs-refresh
|
591
|
+
vfs_refresh: bool = True,
|
592
|
+
other_cmds: list[str] | None = None,
|
593
|
+
) -> Process:
|
594
|
+
"""Mount a remote or directory to a local path.
|
595
|
+
|
596
|
+
Args:
|
597
|
+
src: Remote or directory to mount
|
598
|
+
outdir: Local path to mount to
|
599
|
+
"""
|
600
|
+
other_cmds = other_cmds or []
|
601
|
+
if modtime_strategy is not None:
|
602
|
+
other_cmds.append(f"--{modtime_strategy.value}")
|
603
|
+
if (vfs_cache_mode == "full" or vfs_cache_mode == "writes") and (
|
604
|
+
transfers is not None and "--transfers" not in other_cmds
|
605
|
+
):
|
606
|
+
other_cmds.append("--transfers")
|
607
|
+
other_cmds.append(str(transfers))
|
608
|
+
if dir_cache_time is not None and "--dir-cache-time" not in other_cmds:
|
609
|
+
other_cmds.append("--dir-cache-time")
|
610
|
+
other_cmds.append(dir_cache_time)
|
611
|
+
if (
|
612
|
+
vfs_disk_space_total_size is not None
|
613
|
+
and "--vfs-cache-max-size" not in other_cmds
|
614
|
+
):
|
615
|
+
other_cmds.append("--vfs-cache-max-size")
|
616
|
+
other_cmds.append(vfs_disk_space_total_size)
|
617
|
+
if vfs_refresh and "--vfs-refresh" not in other_cmds:
|
618
|
+
other_cmds.append("--vfs-refresh")
|
619
|
+
if attribute_timeout is not None and "--attr-timeout" not in other_cmds:
|
620
|
+
other_cmds.append("--attr-timeout")
|
621
|
+
other_cmds.append(attribute_timeout)
|
622
|
+
if vfs_read_chunk_streams:
|
623
|
+
other_cmds.append("--vfs-read-chunk-streams")
|
624
|
+
other_cmds.append(str(vfs_read_chunk_streams))
|
625
|
+
if vfs_read_chunk_size:
|
626
|
+
other_cmds.append("--vfs-read-chunk-size")
|
627
|
+
other_cmds.append(vfs_read_chunk_size)
|
628
|
+
if vfs_fast_fingerprint:
|
629
|
+
other_cmds.append("--vfs-fast-fingerprint")
|
630
|
+
|
631
|
+
other_cmds = other_cmds if other_cmds else None
|
632
|
+
return self.mount(
|
633
|
+
url,
|
634
|
+
outdir,
|
635
|
+
allow_writes=allow_writes,
|
636
|
+
vfs_cache_mode=vfs_cache_mode,
|
637
|
+
other_cmds=other_cmds,
|
638
|
+
)
|
639
|
+
|
640
|
+
def serve_webdav(
|
641
|
+
self,
|
642
|
+
src: Remote | Dir | str,
|
643
|
+
user: str,
|
644
|
+
password: str,
|
645
|
+
addr: str = "localhost:2049",
|
646
|
+
allow_other: bool = False,
|
647
|
+
) -> Process:
|
648
|
+
"""Serve a remote or directory via NFS.
|
649
|
+
|
650
|
+
Args:
|
651
|
+
src: Remote or directory to serve
|
652
|
+
addr: Network address and port to serve on (default: localhost:2049)
|
653
|
+
allow_other: Allow other users to access the share
|
654
|
+
|
655
|
+
Returns:
|
656
|
+
Process: The running NFS server process
|
657
|
+
|
658
|
+
Raises:
|
659
|
+
ValueError: If the NFS server fails to start
|
660
|
+
"""
|
661
|
+
src_str = convert_to_str(src)
|
662
|
+
cmd_list: list[str] = ["serve", "webdav", "--addr", addr, src_str]
|
663
|
+
cmd_list.extend(["--user", user, "--pass", password])
|
664
|
+
if allow_other:
|
665
|
+
cmd_list.append("--allow-other")
|
666
|
+
proc = self._launch_process(cmd_list)
|
667
|
+
time.sleep(2) # give it a moment to start
|
668
|
+
if proc.poll() is not None:
|
669
|
+
raise ValueError("NFS serve process failed to start")
|
670
|
+
return proc
|