rclone-api 1.0.88__py2.py3-none-any.whl → 1.0.90__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/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from .completed_process import CompletedProcess
2
- from .config import Config
2
+ from .config import Config, Parsed, Section
3
3
  from .diff import DiffItem, DiffOption, DiffType
4
4
  from .dir import Dir
5
5
  from .dir_listing import DirListing
@@ -9,6 +9,7 @@ from .process import Process
9
9
  from .rclone import Rclone, rclone_verbose
10
10
  from .remote import Remote
11
11
  from .rpath import RPath
12
+ from .s3.types import MultiUploadResult
12
13
  from .types import ListingOption, Order, SizeResult
13
14
 
14
15
  __all__ = [
@@ -30,4 +31,7 @@ __all__ = [
30
31
  "Order",
31
32
  "ListingOption",
32
33
  "SizeResult",
34
+ "Parsed",
35
+ "Section",
36
+ "MultiUploadResult",
33
37
  ]
@@ -0,0 +1,99 @@
1
+ import argparse
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from rclone_api import MultiUploadResult, Rclone
6
+
7
+ _1MB = 1024 * 1024
8
+
9
+
10
+ @dataclass
11
+ class Args:
12
+ config_path: Path
13
+ src: str
14
+ dst: str
15
+ chunk_size_mb: int
16
+ read_concurrent_chunks: int
17
+ retries: int
18
+ save_state_json: Path
19
+
20
+
21
+ def list_files(rclone: Rclone, path: str):
22
+ """List files in a remote path."""
23
+ for dirlisting in rclone.walk(path):
24
+ for file in dirlisting.files:
25
+ print(file.path)
26
+
27
+
28
+ def _parse_args() -> Args:
29
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
30
+ parser.add_argument("src", help="File to copy")
31
+ parser.add_argument("dst", help="Destination file")
32
+ parser.add_argument(
33
+ "--config", help="Path to rclone config file", type=Path, required=True
34
+ )
35
+ parser.add_argument(
36
+ "--chunk-size-mb", help="Chunk size in MB", type=int, default=256
37
+ )
38
+ parser.add_argument(
39
+ "--read-concurrent-chunks",
40
+ help="Maximum number of chunks to read",
41
+ type=int,
42
+ default=4,
43
+ )
44
+ parser.add_argument("--retries", help="Number of retries", type=int, default=3)
45
+ parser.add_argument(
46
+ "--resumable-json",
47
+ help="Path to resumable JSON file",
48
+ type=Path,
49
+ default="resume.json",
50
+ )
51
+
52
+ args = parser.parse_args()
53
+ out = Args(
54
+ config_path=Path(args.config),
55
+ src=args.src,
56
+ dst=args.dst,
57
+ chunk_size_mb=args.chunk_size_mb,
58
+ read_concurrent_chunks=args.read_concurrent_chunks,
59
+ retries=args.retries,
60
+ save_state_json=args.resumable_json,
61
+ )
62
+ return out
63
+
64
+
65
+ def main() -> int:
66
+ """Main entry point."""
67
+ args = _parse_args()
68
+ rclone = Rclone(rclone_conf=args.config_path)
69
+ rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
70
+ src=args.src,
71
+ dst=args.dst,
72
+ chunk_size=args.chunk_size_mb * _1MB,
73
+ concurrent_chunks=args.read_concurrent_chunks,
74
+ retries=args.retries,
75
+ save_state_json=args.save_state_json,
76
+ )
77
+ print(rslt)
78
+ return 0
79
+
80
+
81
+ if __name__ == "__main__":
82
+ import os
83
+ import sys
84
+
85
+ here = Path(__file__).parent
86
+ project_root = here.parent.parent.parent
87
+ print(f"project_root: {project_root}")
88
+ os.chdir(str(project_root))
89
+ cwd = Path(__file__).parent
90
+ print(f"cwd: {cwd}")
91
+ sys.argv.append("--config")
92
+ sys.argv.append("rclone.conf")
93
+ sys.argv.append(
94
+ "45061:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
95
+ )
96
+ sys.argv.append(
97
+ "dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
98
+ )
99
+ main()
rclone_api/config.py CHANGED
@@ -1,4 +1,48 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List
3
+
4
+
5
+ @dataclass
6
+ class Section:
7
+ name: str
8
+ data: Dict[str, str] = field(default_factory=dict)
9
+
10
+ def add(self, key: str, value: str) -> None:
11
+ self.data[key] = value
12
+
13
+ def type(self) -> str:
14
+ return self.data["type"]
15
+
16
+ def provider(self) -> str | None:
17
+ return self.data.get("provider")
18
+
19
+ def access_key_id(self) -> str:
20
+ if "access_key_id" in self.data:
21
+ return self.data["access_key_id"]
22
+ elif "account" in self.data:
23
+ return self.data["account"]
24
+ raise KeyError("No access key found")
25
+
26
+ def secret_access_key(self) -> str:
27
+ # return self.data["secret_access_key"]
28
+ if "secret_access_key" in self.data:
29
+ return self.data["secret_access_key"]
30
+ elif "key" in self.data:
31
+ return self.data["key"]
32
+ raise KeyError("No secret access key found")
33
+
34
+ def endpoint(self) -> str | None:
35
+ return self.data.get("endpoint")
36
+
37
+
38
+ @dataclass
39
+ class Parsed:
40
+ # sections: List[ParsedSection]
41
+ sections: dict[str, Section]
42
+
43
+ @staticmethod
44
+ def parse(content: str) -> "Parsed":
45
+ return parse_rclone_config(content)
2
46
 
3
47
 
4
48
  @dataclass
@@ -6,3 +50,38 @@ class Config:
6
50
  """Rclone configuration dataclass."""
7
51
 
8
52
  text: str
53
+
54
+ def parse(self) -> Parsed:
55
+ return Parsed.parse(self.text)
56
+
57
+
58
+ def parse_rclone_config(content: str) -> Parsed:
59
+ """
60
+ Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
61
+
62
+ Each section in the file starts with a line like [section_name]
63
+ followed by key=value pairs.
64
+ """
65
+ sections: List[Section] = []
66
+ current_section: Section | None = None
67
+
68
+ lines = content.splitlines()
69
+ for line in lines:
70
+ line = line.strip()
71
+ # Skip empty lines and comments (assumed to start with '#' or ';')
72
+ if not line or line.startswith(("#", ";")):
73
+ continue
74
+ # New section header detected
75
+ if line.startswith("[") and line.endswith("]"):
76
+ section_name = line[1:-1].strip()
77
+ current_section = Section(name=section_name)
78
+ sections.append(current_section)
79
+ elif "=" in line and current_section is not None:
80
+ # Parse key and value, splitting only on the first '=' found
81
+ key, value = line.split("=", 1)
82
+ current_section.add(key.strip(), value.strip())
83
+
84
+ data: dict[str, Section] = {}
85
+ for section in sections:
86
+ data[section.name] = section
87
+ return Parsed(sections=data)
rclone_api/group_files.py CHANGED
@@ -68,7 +68,10 @@ class TreeNode:
68
68
  paths_reversed: list[str] = [self.name]
69
69
  node: TreeNode | None = self
70
70
  assert node is not None
71
- while node := node.parent:
71
+ while True:
72
+ node = node.parent
73
+ if node is None:
74
+ break
72
75
  paths_reversed.append(node.name)
73
76
  return "/".join(reversed(paths_reversed))
74
77
 
rclone_api/rclone.py CHANGED
@@ -6,8 +6,10 @@ import os
6
6
  import random
7
7
  import subprocess
8
8
  import time
9
+ import traceback
9
10
  import warnings
10
11
  from concurrent.futures import Future, ThreadPoolExecutor
12
+ from contextlib import contextmanager
11
13
  from fnmatch import fnmatch
12
14
  from pathlib import Path
13
15
  from tempfile import TemporaryDirectory
@@ -15,7 +17,7 @@ from typing import Generator
15
17
 
16
18
  from rclone_api import Dir
17
19
  from rclone_api.completed_process import CompletedProcess
18
- from rclone_api.config import Config
20
+ from rclone_api.config import Config, Parsed, Section
19
21
  from rclone_api.convert import convert_to_filestr_list, convert_to_str
20
22
  from rclone_api.deprecated import deprecated
21
23
  from rclone_api.diff import DiffItem, DiffOption, diff_stream_from_running_process
@@ -26,12 +28,13 @@ from rclone_api.group_files import group_files
26
28
  from rclone_api.process import Process
27
29
  from rclone_api.remote import Remote
28
30
  from rclone_api.rpath import RPath
29
- from rclone_api.types import (
30
- ListingOption,
31
- ModTimeStrategy,
32
- Order,
33
- SizeResult,
31
+ from rclone_api.s3.types import (
32
+ MultiUploadResult,
33
+ S3MutliPartUploadConfig,
34
+ S3Provider,
35
+ S3UploadTarget,
34
36
  )
37
+ from rclone_api.types import ListingOption, ModTimeStrategy, Order, SizeResult
35
38
  from rclone_api.util import (
36
39
  get_check,
37
40
  get_rclone_exe,
@@ -41,6 +44,8 @@ from rclone_api.util import (
41
44
  )
42
45
  from rclone_api.walk import walk
43
46
 
47
+ _IS_WINDOWS = os.name == "nt"
48
+
44
49
 
45
50
  def rclone_verbose(verbose: bool | None) -> bool:
46
51
  if verbose is not None:
@@ -48,6 +53,14 @@ def rclone_verbose(verbose: bool | None) -> bool:
48
53
  return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
49
54
 
50
55
 
56
+ def _to_rclone_conf(config: Config | Path) -> Config:
57
+ if isinstance(config, Path):
58
+ content = config.read_text(encoding="utf-8")
59
+ return Config(content)
60
+ else:
61
+ return config
62
+
63
+
51
64
  class Rclone:
52
65
  def __init__(
53
66
  self, rclone_conf: Path | Config, rclone_exe: Path | None = None
@@ -56,6 +69,7 @@ class Rclone:
56
69
  if not rclone_conf.exists():
57
70
  raise ValueError(f"Rclone config file not found: {rclone_conf}")
58
71
  self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
72
+ self.config: Config = _to_rclone_conf(rclone_conf)
59
73
 
60
74
  def _run(
61
75
  self, cmd: list[str], check: bool = False, capture: bool | None = None
@@ -399,7 +413,7 @@ class Rclone:
399
413
  using_fast_list = "--fast-list" in other_args
400
414
  if using_fast_list:
401
415
  warnings.warn(
402
- "It's not recommended to use --fast-list with copy_files as the entire repository has to be listed"
416
+ "It's not recommended to use --fast-list with copy_files as this will perform poorly on large repositories since the entire repository has to be scanned."
403
417
  )
404
418
 
405
419
  if max_partition_workers > 1:
@@ -654,6 +668,127 @@ class Rclone:
654
668
  except subprocess.CalledProcessError:
655
669
  return False
656
670
 
671
+ def copy_file_resumable_s3(
672
+ self,
673
+ src: str,
674
+ dst: str,
675
+ save_state_json: Path,
676
+ chunk_size: int = 16
677
+ * 1024
678
+ * 1024, # This setting will scale the performance of the upload
679
+ concurrent_chunks: int = 4, # This setting will scale the performance of the upload
680
+ retries: int = 3,
681
+ max_chunks_before_suspension: int | None = None,
682
+ ) -> MultiUploadResult:
683
+ """For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
684
+ from rclone_api.s3.api import S3Client
685
+ from rclone_api.s3.create import S3Credentials
686
+ from rclone_api.util import S3PathInfo, split_s3_path
687
+
688
+ other_args: list[str] = [
689
+ "--no-modtime",
690
+ "--vfs-read-wait",
691
+ "1s",
692
+ "--vfs-disk-space-total-size",
693
+ str(2 * chunk_size * concurrent_chunks), # purge quickly.
694
+ "--vfs-read-chunk-size",
695
+ str(chunk_size),
696
+ "--vfs-read-chunk-size-limit",
697
+ str(chunk_size * concurrent_chunks),
698
+ "--vfs-read-chunk-streams",
699
+ str(concurrent_chunks),
700
+ "--vfs-fast-fingerprint",
701
+ ]
702
+ mount_path = Path("rclone_api_upload_mount")
703
+ src_path = Path(src)
704
+ name = src_path.name
705
+
706
+ parent_path = str(src_path.parent.as_posix())
707
+ with self.scoped_mount(
708
+ parent_path,
709
+ mount_path,
710
+ use_links=True,
711
+ vfs_cache_mode="minimal",
712
+ other_args=other_args,
713
+ ):
714
+ # raise NotImplementedError("Not implemented yet")
715
+
716
+ path_info: S3PathInfo = split_s3_path(dst)
717
+ remote = path_info.remote
718
+ bucket_name = path_info.bucket
719
+ s3_key = path_info.key
720
+ parsed: Parsed = self.config.parse()
721
+ sections: dict[str, Section] = parsed.sections
722
+ if remote not in sections:
723
+ raise ValueError(
724
+ f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
725
+ )
726
+
727
+ section: Section = sections[remote]
728
+ dst_type = section.type()
729
+ if dst_type != "s3" and dst_type != "b2":
730
+ raise ValueError(
731
+ f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
732
+ )
733
+
734
+ def get_provider_str(section=section) -> str | None:
735
+ type: str = section.type()
736
+ provider: str | None = section.provider()
737
+ if provider is not None:
738
+ return provider
739
+ if type == "b2":
740
+ return S3Provider.BACKBLAZE.value
741
+ if type != "s3":
742
+ raise ValueError(f"Remote {remote} is not an S3 remote")
743
+ return S3Provider.S3.value
744
+
745
+ provider: str = get_provider_str() or S3Provider.S3.value
746
+ provider_enum = S3Provider.from_str(provider)
747
+
748
+ s3_creds: S3Credentials = S3Credentials(
749
+ provider=provider_enum,
750
+ access_key_id=section.access_key_id(),
751
+ secret_access_key=section.secret_access_key(),
752
+ endpoint_url=section.endpoint(),
753
+ )
754
+
755
+ client = S3Client(s3_creds)
756
+ config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
757
+ chunk_size=chunk_size,
758
+ retries=retries,
759
+ resume_path_json=save_state_json,
760
+ max_chunks_before_suspension=max_chunks_before_suspension,
761
+ )
762
+
763
+ src_file = mount_path / name
764
+
765
+ print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
766
+ print(f"Source: {src_path}")
767
+ print(f"bucket_name: {bucket_name}")
768
+ print(f"upload_config: {config}")
769
+
770
+ upload_target: S3UploadTarget
771
+ upload_config: S3MutliPartUploadConfig
772
+
773
+ upload_target = S3UploadTarget(
774
+ bucket_name=bucket_name,
775
+ src_file=src_file,
776
+ s3_key=s3_key,
777
+ )
778
+
779
+ upload_config = S3MutliPartUploadConfig(
780
+ chunk_size=chunk_size,
781
+ retries=retries,
782
+ resume_path_json=save_state_json,
783
+ max_chunks_before_suspension=max_chunks_before_suspension,
784
+ )
785
+
786
+ out: MultiUploadResult = client.upload_file_multipart(
787
+ upload_target=upload_target,
788
+ upload_config=upload_config
789
+ )
790
+ return out
791
+
657
792
  def copy_dir(
658
793
  self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
659
794
  ) -> CompletedProcess:
@@ -682,10 +817,10 @@ class Rclone:
682
817
  self,
683
818
  src: Remote | Dir | str,
684
819
  outdir: Path,
685
- allow_writes=False,
686
- use_links=True,
687
- vfs_cache_mode="full",
688
- other_cmds: list[str] | None = None,
820
+ allow_writes: bool | None = False,
821
+ use_links: bool | None = None,
822
+ vfs_cache_mode: str | None = None,
823
+ other_args: list[str] | None = None,
689
824
  ) -> Process:
690
825
  """Mount a remote or directory to a local path.
691
826
 
@@ -699,6 +834,9 @@ class Rclone:
699
834
  Raises:
700
835
  subprocess.CalledProcessError: If the mount operation fails
701
836
  """
837
+ allow_writes = allow_writes or False
838
+ use_links = use_links or True
839
+ vfs_cache_mode = vfs_cache_mode or "full"
702
840
  if outdir.exists():
703
841
  is_empty = not list(outdir.iterdir())
704
842
  if not is_empty:
@@ -706,12 +844,13 @@ class Rclone:
706
844
  f"Mount directory already exists and is not empty: {outdir}"
707
845
  )
708
846
  outdir.rmdir()
709
- try:
847
+
848
+ if _IS_WINDOWS:
849
+ # Windows -> Must create parent directories only if they don't exist
710
850
  outdir.parent.mkdir(parents=True, exist_ok=True)
711
- except PermissionError:
712
- warnings.warn(
713
- f"Permission error creating parent directory: {outdir.parent}"
714
- )
851
+ else:
852
+ # Linux -> Must create parent directories and the directory itself
853
+ outdir.mkdir(parents=True, exist_ok=True)
715
854
  src_str = convert_to_str(src)
716
855
  cmd_list: list[str] = ["mount", src_str, str(outdir)]
717
856
  if not allow_writes:
@@ -721,19 +860,50 @@ class Rclone:
721
860
  if vfs_cache_mode:
722
861
  cmd_list.append("--vfs-cache-mode")
723
862
  cmd_list.append(vfs_cache_mode)
724
- if other_cmds:
725
- cmd_list += other_cmds
863
+ if other_args:
864
+ cmd_list += other_args
726
865
  proc = self._launch_process(cmd_list)
727
866
  wait_for_mount(outdir, proc)
728
867
  return proc
729
868
 
869
+ @contextmanager
870
+ def scoped_mount(
871
+ self,
872
+ src: Remote | Dir | str,
873
+ outdir: Path,
874
+ allow_writes: bool | None = None,
875
+ use_links: bool | None = None,
876
+ vfs_cache_mode: str | None = None,
877
+ other_args: list[str] | None = None,
878
+ ) -> Generator[Process, None, None]:
879
+ """Like mount, but can be used in a context manager."""
880
+ proc = self.mount(
881
+ src,
882
+ outdir,
883
+ allow_writes=allow_writes,
884
+ use_links=use_links,
885
+ vfs_cache_mode=vfs_cache_mode,
886
+ other_args=other_args,
887
+ )
888
+ try:
889
+ yield proc
890
+ except Exception as e:
891
+ stack_trace = traceback.format_exc()
892
+ warnings.warn(f"Error in scoped_mount: {e}\n\nStack Trace:\n{stack_trace}")
893
+ raise
894
+ finally:
895
+ if proc.poll() is None:
896
+ proc.terminate()
897
+ proc.wait()
898
+
899
+ @deprecated("mount")
730
900
  def mount_webdav(
731
901
  self,
732
902
  url: str,
733
903
  outdir: Path,
734
- vfs_cache_mode="full",
904
+ vfs_cache_mode: str | None = None,
735
905
  vfs_disk_space_total_size: str | None = "10G",
736
- other_cmds: list[str] | None = None,
906
+ other_args: list[str] | None = None,
737
907
  ) -> Process:
738
908
  """Mount a remote or directory to a local path.
739
909
 
@@ -747,6 +917,20 @@ class Rclone:
747
917
  Raises:
748
918
  subprocess.CalledProcessError: If the mount operation fails
749
919
  """
920
+ other_args = other_args or []
921
+ if vfs_cache_mode is None:
922
+ if "--vfs-cache-mode" in other_args:
923
+ pass
924
+ else:
925
+ vfs_cache_mode = "full"
926
+ elif "--vfs-cache-mode" in other_args:
927
+ warnings.warn(
928
+ f"vfs_cache_mode is set to {vfs_cache_mode} but --vfs-cache-mode is already in other_args"
929
+ )
930
+ idx = other_args.index("--vfs-cache-mode")
931
+ other_args.pop(idx)
932
+ other_args.pop(idx) # also the next value which will be the cache mode.
933
+
750
934
  if outdir.exists():
751
935
  is_empty = not list(outdir.iterdir())
752
936
  if not is_empty:
@@ -757,10 +941,11 @@ class Rclone:
757
941
 
758
942
  src_str = url
759
943
  cmd_list: list[str] = ["mount", src_str, str(outdir)]
760
- cmd_list.append("--vfs-cache-mode")
761
- cmd_list.append(vfs_cache_mode)
762
- if other_cmds:
763
- cmd_list += other_cmds
944
+ if vfs_cache_mode:
945
+ cmd_list.append("--vfs-cache-mode")
946
+ cmd_list.append(vfs_cache_mode)
947
+ if other_args:
948
+ cmd_list += other_args
764
949
  if vfs_disk_space_total_size is not None:
765
950
  cmd_list.append("--vfs-cache-max-size")
766
951
  cmd_list.append(vfs_disk_space_total_size)
@@ -768,6 +953,7 @@ class Rclone:
768
953
  wait_for_mount(outdir, proc)
769
954
  return proc
770
955
 
956
+ # Settings optimized for s3.
771
957
  def mount_s3(
772
958
  self,
773
959
  url: str,
@@ -789,7 +975,7 @@ class Rclone:
789
975
  vfs_fast_fingerprint: bool = True,
790
976
  # vfs-refresh
791
977
  vfs_refresh: bool = True,
792
- other_cmds: list[str] | None = None,
978
+ other_args: list[str] | None = None,
793
979
  ) -> Process:
794
980
  """Mount a remote or directory to a local path.
795
981
 
@@ -797,44 +983,44 @@ class Rclone:
797
983
  src: Remote or directory to mount
798
984
  outdir: Local path to mount to
799
985
  """
800
- other_cmds = other_cmds or []
986
+ other_args = other_args or []
801
987
  if modtime_strategy is not None:
802
- other_cmds.append(f"--{modtime_strategy.value}")
988
+ other_args.append(f"--{modtime_strategy.value}")
803
989
  if (vfs_cache_mode == "full" or vfs_cache_mode == "writes") and (
804
- transfers is not None and "--transfers" not in other_cmds
990
+ transfers is not None and "--transfers" not in other_args
805
991
  ):
806
- other_cmds.append("--transfers")
807
- other_cmds.append(str(transfers))
808
- if dir_cache_time is not None and "--dir-cache-time" not in other_cmds:
809
- other_cmds.append("--dir-cache-time")
810
- other_cmds.append(dir_cache_time)
992
+ other_args.append("--transfers")
993
+ other_args.append(str(transfers))
994
+ if dir_cache_time is not None and "--dir-cache-time" not in other_args:
995
+ other_args.append("--dir-cache-time")
996
+ other_args.append(dir_cache_time)
811
997
  if (
812
998
  vfs_disk_space_total_size is not None
813
- and "--vfs-cache-max-size" not in other_cmds
999
+ and "--vfs-cache-max-size" not in other_args
814
1000
  ):
815
- other_cmds.append("--vfs-cache-max-size")
816
- other_cmds.append(vfs_disk_space_total_size)
817
- if vfs_refresh and "--vfs-refresh" not in other_cmds:
818
- other_cmds.append("--vfs-refresh")
819
- if attribute_timeout is not None and "--attr-timeout" not in other_cmds:
820
- other_cmds.append("--attr-timeout")
821
- other_cmds.append(attribute_timeout)
1001
+ other_args.append("--vfs-cache-max-size")
1002
+ other_args.append(vfs_disk_space_total_size)
1003
+ if vfs_refresh and "--vfs-refresh" not in other_args:
1004
+ other_args.append("--vfs-refresh")
1005
+ if attribute_timeout is not None and "--attr-timeout" not in other_args:
1006
+ other_args.append("--attr-timeout")
1007
+ other_args.append(attribute_timeout)
822
1008
  if vfs_read_chunk_streams:
823
- other_cmds.append("--vfs-read-chunk-streams")
824
- other_cmds.append(str(vfs_read_chunk_streams))
1009
+ other_args.append("--vfs-read-chunk-streams")
1010
+ other_args.append(str(vfs_read_chunk_streams))
825
1011
  if vfs_read_chunk_size:
826
- other_cmds.append("--vfs-read-chunk-size")
827
- other_cmds.append(vfs_read_chunk_size)
1012
+ other_args.append("--vfs-read-chunk-size")
1013
+ other_args.append(vfs_read_chunk_size)
828
1014
  if vfs_fast_fingerprint:
829
- other_cmds.append("--vfs-fast-fingerprint")
1015
+ other_args.append("--vfs-fast-fingerprint")
830
1016
 
831
- other_cmds = other_cmds if other_cmds else None
1017
+ other_args = other_args if other_args else None
832
1018
  return self.mount(
833
1019
  url,
834
1020
  outdir,
835
1021
  allow_writes=allow_writes,
836
1022
  vfs_cache_mode=vfs_cache_mode,
837
- other_cmds=other_cmds,
1023
+ other_args=other_args,
838
1024
  )
839
1025
 
840
1026
  def serve_webdav(
@@ -844,6 +1030,7 @@ class Rclone:
844
1030
  password: str,
845
1031
  addr: str = "localhost:2049",
846
1032
  allow_other: bool = False,
1033
+ other_args: list[str] | None = None,
847
1034
  ) -> Process:
848
1035
  """Serve a remote or directory via NFS.
849
1036
 
@@ -863,6 +1050,8 @@ class Rclone:
863
1050
  cmd_list.extend(["--user", user, "--pass", password])
864
1051
  if allow_other:
865
1052
  cmd_list.append("--allow-other")
1053
+ if other_args:
1054
+ cmd_list += other_args
866
1055
  proc = self._launch_process(cmd_list)
867
1056
  time.sleep(2) # give it a moment to start
868
1057
  if proc.poll() is not None:
@@ -883,7 +1072,7 @@ class Rclone:
883
1072
  check = get_check(check)
884
1073
  if fast_list or (other_args and "--fast-list" in other_args):
885
1074
  warnings.warn(
886
- "It's not recommended to use --fast-list with size_files as the entire repository has to be listed"
1075
+ "It's not recommended to use --fast-list with size_files as this will perform poorly on large repositories since the entire repository has to be scanned."
887
1076
  )
888
1077
  files = list(files)
889
1078
  all_files: list[File] = []