pymover 0.3.0.dev3__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.
- pymover-0.3.0.dev3/LICENSE +19 -0
- pymover-0.3.0.dev3/PKG-INFO +54 -0
- pymover-0.3.0.dev3/README.md +30 -0
- pymover-0.3.0.dev3/pymover/__init__.py +0 -0
- pymover-0.3.0.dev3/pymover/__version__.py +7 -0
- pymover-0.3.0.dev3/pymover/cli.py +23 -0
- pymover-0.3.0.dev3/pymover/pymover.py +362 -0
- pymover-0.3.0.dev3/pymover/utils.py +9 -0
- pymover-0.3.0.dev3/pymover.egg-info/PKG-INFO +54 -0
- pymover-0.3.0.dev3/pymover.egg-info/SOURCES.txt +16 -0
- pymover-0.3.0.dev3/pymover.egg-info/dependency_links.txt +1 -0
- pymover-0.3.0.dev3/pymover.egg-info/requires.txt +8 -0
- pymover-0.3.0.dev3/pymover.egg-info/top_level.txt +1 -0
- pymover-0.3.0.dev3/pyproject.toml +50 -0
- pymover-0.3.0.dev3/setup.cfg +9 -0
- pymover-0.3.0.dev3/tests/test_cli.py +107 -0
- pymover-0.3.0.dev3/tests/test_pymover.py +787 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymover
|
|
3
|
+
Version: 0.3.0.dev3
|
|
4
|
+
Summary: Custom move command with progress bar
|
|
5
|
+
Author: Thorbjoern Mumme
|
|
6
|
+
License: n.a.
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/thomumme/file-mover
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/thomumme/file-mover
|
|
9
|
+
Keywords: file,move,copy,progress
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: loguru
|
|
18
|
+
Requires-Dist: click
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: randomfiletree; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# FileMover
|
|
26
|
+
|
|
27
|
+
FileMover is a Python package that provides command-line tools for moving and copying files with progress bars. It supports both single file operations and batch operations on multiple files.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
You can install FileMover using pip:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install pymover
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install the latest development version from GitLab:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://gitlab.com/thomumme/file-mover.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Changelog
|
|
50
|
+
|
|
51
|
+
See [CHANGELOG.md](CHANGELOG.md) for a history of changes.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# FileMover
|
|
2
|
+
|
|
3
|
+
FileMover is a Python package that provides command-line tools for moving and copying files with progress bars. It supports both single file operations and batch operations on multiple files.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
You can install FileMover using pip:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pymover
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install the latest development version from GitLab:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://gitlab.com/thomumme/file-mover.git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Changelog
|
|
26
|
+
|
|
27
|
+
See [CHANGELOG.md](CHANGELOG.md) for a history of changes.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from .pymover import move_with_progress, copy_with_progress
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
@click.argument('src', type=click.Path(), nargs=-1)
|
|
8
|
+
@click.argument('dst', type=click.Path())
|
|
9
|
+
@click.option("--simulate", is_flag=True, default=False)
|
|
10
|
+
def move(**kwargs):
|
|
11
|
+
move_with_progress(src=kwargs['src'], dst=kwargs['dst'], simulate=kwargs['simulate'])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command()
|
|
15
|
+
@click.argument('src', type=click.Path(), nargs=-1)
|
|
16
|
+
@click.argument('dst', type=click.Path())
|
|
17
|
+
@click.option("--simulate", is_flag=True, default=False)
|
|
18
|
+
def copy(**kwargs):
|
|
19
|
+
copy_with_progress(src=kwargs['src'], dst=kwargs['dst'])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__':
|
|
23
|
+
move()
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
from typing import Union, Tuple
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
from .utils import to_list
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileMover:
|
|
17
|
+
def __init__(self, interactive_mode: bool, conflict_resolution: str = "ya", create_missing_parents: bool = False):
|
|
18
|
+
self._is_interactive: bool = interactive_mode
|
|
19
|
+
|
|
20
|
+
# if we are in interactive mode we set the conflict resolution to undefined to prompt the options
|
|
21
|
+
# otherwise we just set what has been passed
|
|
22
|
+
self._conflict_mode: str = "" if interactive_mode else conflict_resolution
|
|
23
|
+
|
|
24
|
+
# in non interactive mode the option "y" or "n" does not really make sense, because we have to apply it to all copy/move calls
|
|
25
|
+
# so we simply change it to "ya" or "na" and it gets handled as expected in the copy/move functions
|
|
26
|
+
if not interactive_mode:
|
|
27
|
+
self._conflict_mode += "a" if self._conflict_mode in {"y", "n"} else ""
|
|
28
|
+
|
|
29
|
+
self._create_missing_parents = create_missing_parents
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def prompt_overwrite_conflict(self, target_file: Path):
|
|
33
|
+
"""
|
|
34
|
+
Prompts the user to handle a file overwrite conflict.
|
|
35
|
+
Returns the key (e.g., 'ya') selected by the user.
|
|
36
|
+
"""
|
|
37
|
+
options = {
|
|
38
|
+
"y": "Yes",
|
|
39
|
+
"n": "No",
|
|
40
|
+
"ya": "Yes to all",
|
|
41
|
+
"na": "No to all",
|
|
42
|
+
"s": "Skip Conflict",
|
|
43
|
+
"sa": "Skip All Conflicts"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Signal the conflict
|
|
47
|
+
logger.warning(f"Conflict Detected: Do you want to overwrite the existing file: '{target_file}'")
|
|
48
|
+
|
|
49
|
+
# Print the clean menu
|
|
50
|
+
logger.opt(raw=True).info(f"{'-' * 40}\n")
|
|
51
|
+
for key, description in options.items():
|
|
52
|
+
logger.opt(raw=True).info(f" [{key.ljust(2)}] : {description}\n")
|
|
53
|
+
logger.opt(raw=True).info(f"{'-' * 40}\n")
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
try:
|
|
57
|
+
choice = input("Select an option: ").strip().lower()
|
|
58
|
+
|
|
59
|
+
if choice in options:
|
|
60
|
+
# Return the key instead of the descriptive value
|
|
61
|
+
logger.success(f"Action confirmed: {choice}")
|
|
62
|
+
return choice
|
|
63
|
+
|
|
64
|
+
logger.error(f"Invalid input '{choice}'. Valid: {', '.join(options.keys())}")
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
logger.critical("\nOperation aborted by user.")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
def _get_source_size(self, src: List[Union[Path, str]]) -> int:
|
|
70
|
+
size: int = 0
|
|
71
|
+
for s in src:
|
|
72
|
+
if s.is_dir():
|
|
73
|
+
size += self._get_source_size(s.glob('*'))
|
|
74
|
+
else:
|
|
75
|
+
size += s.stat().st_size
|
|
76
|
+
return size
|
|
77
|
+
|
|
78
|
+
def _copy_file(self, src: Path, dst: Path, progres_bar: tqdm, simulate: bool = False):
|
|
79
|
+
assert isinstance(src, Path)
|
|
80
|
+
assert isinstance(dst, Path)
|
|
81
|
+
if dst.is_dir():
|
|
82
|
+
dst = dst / Path(src.name)
|
|
83
|
+
|
|
84
|
+
if simulate:
|
|
85
|
+
logger.info(f"Copying {src} to {dst}")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if dst.exists():
|
|
89
|
+
|
|
90
|
+
if not self._conflict_mode:
|
|
91
|
+
# no conflict mode is set, so we ask what to do
|
|
92
|
+
self._conflict_mode = self.prompt_overwrite_conflict(dst)
|
|
93
|
+
|
|
94
|
+
# now we need to evaluate the conflict mode
|
|
95
|
+
if self._conflict_mode in {"sa", "s", "na", "n"}:
|
|
96
|
+
logger.info(f"Skipping file {src}, because it already exists at target destination")
|
|
97
|
+
return
|
|
98
|
+
elif self._conflict_mode in {"ya", "y"}:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
raise RuntimeError(f"Unknown conflict mode '{self._conflict_mode}'")
|
|
102
|
+
|
|
103
|
+
# we need to cleanup the conflict mode, in case it is not a "* to all" option
|
|
104
|
+
if len(self._conflict_mode) < 2:
|
|
105
|
+
self._conflict_mode = ""
|
|
106
|
+
|
|
107
|
+
# create missing parent directories
|
|
108
|
+
if self._create_missing_parents:
|
|
109
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
# progres_bar.set_description(desc=src.name)
|
|
112
|
+
inner_bar = tqdm(total=src.stat().st_size, unit='B', unit_scale=True, position=1, leave=False, desc=src.name)
|
|
113
|
+
with open(src, 'rb') as fsrc:
|
|
114
|
+
with open(dst, 'wb') as fdst:
|
|
115
|
+
# with tqdm(total=size, unit='B', unit_scale=True, desc=f'Copying {src} to {dst}') as pbar:
|
|
116
|
+
while True:
|
|
117
|
+
chunk = fsrc.read(4096)
|
|
118
|
+
if not chunk:
|
|
119
|
+
break
|
|
120
|
+
fdst.write(chunk)
|
|
121
|
+
fdst.flush()
|
|
122
|
+
progres_bar.update(len(chunk))
|
|
123
|
+
inner_bar.update(len(chunk))
|
|
124
|
+
|
|
125
|
+
def _copy_directory(self, src: Path, dst: Path, progres_bar: tqdm, simulate: bool = False):
|
|
126
|
+
# check if the target directory exists, if not create it
|
|
127
|
+
if not dst.exists():
|
|
128
|
+
if simulate:
|
|
129
|
+
logger.info(f"Creating directory {dst}")
|
|
130
|
+
else:
|
|
131
|
+
dst.mkdir()
|
|
132
|
+
# get the content of the directory
|
|
133
|
+
content = src.glob('*')
|
|
134
|
+
for c in content:
|
|
135
|
+
if c.is_dir():
|
|
136
|
+
self._copy_directory(c, dst / c.name, progres_bar, simulate=simulate)
|
|
137
|
+
elif c.is_file():
|
|
138
|
+
self._copy_file(c, dst / c.name, progres_bar, simulate=simulate)
|
|
139
|
+
else:
|
|
140
|
+
raise NotImplementedError("This case is not handled yet")
|
|
141
|
+
|
|
142
|
+
def copy_with_progress(self, src: Tuple[Union[Path, str]], dst: Union[Path, str], simulate: bool = False, recursive: bool = False,
|
|
143
|
+
progress_bar=None):
|
|
144
|
+
|
|
145
|
+
assert isinstance(dst, (list, Path, str))
|
|
146
|
+
|
|
147
|
+
src = to_list(src)
|
|
148
|
+
src = [Path(x) for x in src]
|
|
149
|
+
dst = to_list(dst)
|
|
150
|
+
dst = [Path(x) for x in dst]
|
|
151
|
+
|
|
152
|
+
if all(s.is_dir() for s in src) and not recursive:
|
|
153
|
+
raise RuntimeError("Recursive mode must be enabled when copying a directory")
|
|
154
|
+
|
|
155
|
+
if len(dst) == 1 and len(src) > 1:
|
|
156
|
+
if not dst[0].is_dir():
|
|
157
|
+
raise RuntimeError("Target must be a directory when a copying multiple sources to the same destination")
|
|
158
|
+
|
|
159
|
+
# with the previous check we made sure that the destination is a directory
|
|
160
|
+
# and here we create a list of targets, which are all the same, one element for each source
|
|
161
|
+
dst = [dst[0] for s in src]
|
|
162
|
+
|
|
163
|
+
assert isinstance(src, List)
|
|
164
|
+
assert len(src) == len(dst)
|
|
165
|
+
for s, d in zip(src, dst):
|
|
166
|
+
if s.is_dir() and d.is_file():
|
|
167
|
+
raise RuntimeError("Target must be a directory when a copying directory")
|
|
168
|
+
|
|
169
|
+
size = self._get_source_size(src)
|
|
170
|
+
|
|
171
|
+
if progress_bar is None:
|
|
172
|
+
progress_bar = tqdm(total=size, unit='B', unit_scale=True)
|
|
173
|
+
|
|
174
|
+
for s, d in zip(src, dst):
|
|
175
|
+
if s.is_file():
|
|
176
|
+
if d.is_dir():
|
|
177
|
+
new_destination = d / s.name
|
|
178
|
+
else:
|
|
179
|
+
new_destination = d
|
|
180
|
+
self._copy_file(s, new_destination, progress_bar, simulate=simulate)
|
|
181
|
+
else:
|
|
182
|
+
# in case the target dir already exists, we want to copy the source into the existing directory
|
|
183
|
+
# otherwise we copy the source directory to the destination (and rename it)
|
|
184
|
+
_target = d / s.name if d.is_dir() else d
|
|
185
|
+
self._copy_directory(s, _target, progress_bar, simulate=simulate)
|
|
186
|
+
|
|
187
|
+
def find_mount_point(self, path):
|
|
188
|
+
path = os.path.abspath(path)
|
|
189
|
+
while not os.path.ismount(path):
|
|
190
|
+
path = os.path.dirname(path)
|
|
191
|
+
return path
|
|
192
|
+
|
|
193
|
+
def use_system_move(self, src, dst) -> bool:
|
|
194
|
+
if platform.system() == 'Windows':
|
|
195
|
+
if src.absolute().anchor == dst.absolute().anchor:
|
|
196
|
+
# when moving files on the same drive we simply edit the file system entry
|
|
197
|
+
# and do not need to copy the file
|
|
198
|
+
return True
|
|
199
|
+
else:
|
|
200
|
+
return False
|
|
201
|
+
elif platform.system() == 'Linux':
|
|
202
|
+
src_mount = self.find_mount_point(src)
|
|
203
|
+
dst_mount = self.find_mount_point(dst)
|
|
204
|
+
if src_mount == dst_mount:
|
|
205
|
+
return True
|
|
206
|
+
else:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
else:
|
|
210
|
+
logger.warning(f"Unsupported operating system '{platform.system()}'. Using working directory...")
|
|
211
|
+
|
|
212
|
+
def _move_file(self, src: Path, dst: Path, progres_bar: tqdm, simulate: bool = False,
|
|
213
|
+
force_custom_move: bool = False):
|
|
214
|
+
if self.use_system_move(src, dst) and not force_custom_move:
|
|
215
|
+
# when moving files on the same drive we simply edit the file system entry
|
|
216
|
+
# and do not need to copy the file
|
|
217
|
+
progres_bar.set_description(desc=src.name)
|
|
218
|
+
|
|
219
|
+
if dst.exists():
|
|
220
|
+
|
|
221
|
+
if not self._conflict_mode:
|
|
222
|
+
# no conflict mode is set, so we ask what to do
|
|
223
|
+
self._conflict_mode = self.prompt_overwrite_conflict(dst)
|
|
224
|
+
|
|
225
|
+
# now we need to evaluate the conflict mode
|
|
226
|
+
if self._conflict_mode in {"sa", "s", "na", "n"}:
|
|
227
|
+
logger.info(f"Skipping file {src}, because it already exists at target destination")
|
|
228
|
+
return
|
|
229
|
+
elif self._conflict_mode in {"ya", "y"}:
|
|
230
|
+
pass
|
|
231
|
+
else:
|
|
232
|
+
raise RuntimeError(f"Unknown conflict mode '{self._conflict_mode}'")
|
|
233
|
+
|
|
234
|
+
# we need to cleanup the conflict mode, in case it is not a "* to all" option
|
|
235
|
+
if len(self._conflict_mode) < 2:
|
|
236
|
+
self._conflict_mode = ""
|
|
237
|
+
|
|
238
|
+
# create missing parent directories
|
|
239
|
+
if self._create_missing_parents:
|
|
240
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
shutil.move(src, dst)
|
|
243
|
+
progres_bar.update(dst.stat().st_size)
|
|
244
|
+
else:
|
|
245
|
+
self.copy_with_progress([src], dst, simulate=simulate, progress_bar=progres_bar)
|
|
246
|
+
os.remove(src)
|
|
247
|
+
|
|
248
|
+
def _move_directory(self, src: Path,
|
|
249
|
+
dst: Path,
|
|
250
|
+
progres_bar: tqdm,
|
|
251
|
+
simulate: bool = False,
|
|
252
|
+
force_custom_move: bool = False):
|
|
253
|
+
if not dst.exists():
|
|
254
|
+
dst.mkdir()
|
|
255
|
+
content = src.glob('*')
|
|
256
|
+
for c in content:
|
|
257
|
+
if c.is_dir():
|
|
258
|
+
self._move_directory(c, dst / c.name, progres_bar=progres_bar, simulate=simulate,
|
|
259
|
+
force_custom_move=force_custom_move)
|
|
260
|
+
# c.rmdir()
|
|
261
|
+
elif c.is_file():
|
|
262
|
+
self._move_file(c, dst / c.name, progres_bar=progres_bar, simulate=simulate,
|
|
263
|
+
force_custom_move=force_custom_move)
|
|
264
|
+
# The root directory gets deleted after it has been moved
|
|
265
|
+
src.rmdir()
|
|
266
|
+
|
|
267
|
+
def move_with_progress(self, src: Tuple[Union[Path, str]], dst: Union[Path, str], force_custom_move: bool = False, simulate: bool = False):
|
|
268
|
+
assert isinstance(dst, (list, Path, str))
|
|
269
|
+
|
|
270
|
+
src = to_list(src)
|
|
271
|
+
src = [Path(x) for x in src]
|
|
272
|
+
dst = to_list(dst)
|
|
273
|
+
dst = [Path(x) for x in dst]
|
|
274
|
+
|
|
275
|
+
if len(dst) == 1 and len(src) > 1:
|
|
276
|
+
if not dst[0].is_dir():
|
|
277
|
+
raise FileNotFoundError("Target must be a directory when a copying multiple sources to the same destination")
|
|
278
|
+
|
|
279
|
+
if len(dst) == 1 and len(src) > 1:
|
|
280
|
+
if not dst[0].is_dir():
|
|
281
|
+
raise RuntimeError("Target must be a directory when a moving multiple objects to the same destination")
|
|
282
|
+
|
|
283
|
+
# with the previous check we made sure that the destination is a directory
|
|
284
|
+
# and here we create a list of targets, which are all the same, one element for each source
|
|
285
|
+
dst = [dst[0] for s in src]
|
|
286
|
+
|
|
287
|
+
assert len(src) == len(dst)
|
|
288
|
+
for s, d in zip(src, dst):
|
|
289
|
+
if s.is_dir() and d.is_file():
|
|
290
|
+
raise RuntimeError("Target must be a directory when a copying directory")
|
|
291
|
+
|
|
292
|
+
size = self._get_source_size(src)
|
|
293
|
+
progress_bar = tqdm(total=size, unit='B', unit_scale=True, position=0)
|
|
294
|
+
|
|
295
|
+
for s, d in zip(src, dst):
|
|
296
|
+
if s.is_file():
|
|
297
|
+
if d.is_dir():
|
|
298
|
+
new_destination = d / s.name
|
|
299
|
+
else:
|
|
300
|
+
new_destination = d
|
|
301
|
+
self._move_file(s, new_destination, progress_bar, simulate=simulate)
|
|
302
|
+
else:
|
|
303
|
+
# in case the target dir already exists, we want to copy the source into the existing directory
|
|
304
|
+
# otherwise we copy the source directory to the destination (and rename it)
|
|
305
|
+
_target = d / s.name if d.is_dir() else d
|
|
306
|
+
self._move_directory(s, _target, progress_bar, simulate=simulate)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def copy_with_progress(src: Tuple[Union[Path, str]], dst: Union[Path, str],
|
|
310
|
+
interactive: bool = False, conflict_resoluation: str = "ya", create_missing_parents: bool = False, recursive: bool = False, simulate: bool = False,
|
|
311
|
+
progress_bar=None):
|
|
312
|
+
FileMover(interactive_mode=interactive,
|
|
313
|
+
conflict_resolution=conflict_resoluation,
|
|
314
|
+
create_missing_parents=create_missing_parents).copy_with_progress(src,
|
|
315
|
+
dst,
|
|
316
|
+
recursive=recursive,
|
|
317
|
+
simulate=simulate,
|
|
318
|
+
progress_bar=progress_bar)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def move_with_progress(src: Tuple[Union[Path, str]], dst: Union[Path, str], force_custom_move: bool = False, interactive: bool = False, conflict_resoluation: str = "ya",
|
|
322
|
+
simulate: bool = False, create_missing_parents: bool = False, ):
|
|
323
|
+
FileMover(interactive_mode=interactive,
|
|
324
|
+
conflict_resolution=conflict_resoluation,
|
|
325
|
+
create_missing_parents=create_missing_parents).move_with_progress(src, dst,
|
|
326
|
+
force_custom_move=force_custom_move,
|
|
327
|
+
simulate=simulate)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def start(
|
|
331
|
+
source_glob: List[str] = ("*/*",),
|
|
332
|
+
target: str = "new",
|
|
333
|
+
amount: int = 50,
|
|
334
|
+
selector_func=None,
|
|
335
|
+
dry_run: bool = False) -> List[Path]:
|
|
336
|
+
target_directory = Path(target)
|
|
337
|
+
amount = amount
|
|
338
|
+
|
|
339
|
+
source_files = []
|
|
340
|
+
for g in source_glob:
|
|
341
|
+
source_files.extend(glob.glob(g))
|
|
342
|
+
# source_files.extend(list(Path().glob(g)))
|
|
343
|
+
source_files = [Path(x) for x in source_files]
|
|
344
|
+
|
|
345
|
+
logger.info(f"Total amount of files: {len(source_files)}")
|
|
346
|
+
|
|
347
|
+
source_files = selector_func(source_files, amount)
|
|
348
|
+
|
|
349
|
+
logger.debug("Selected files")
|
|
350
|
+
for i, f in enumerate(source_files):
|
|
351
|
+
logger.debug(f"[{i}] {f}")
|
|
352
|
+
|
|
353
|
+
if dry_run:
|
|
354
|
+
for f in source_files:
|
|
355
|
+
destination = Path(target) / f.name
|
|
356
|
+
logger.info(f"'{f}' would be moved to '{destination}'")
|
|
357
|
+
|
|
358
|
+
else:
|
|
359
|
+
|
|
360
|
+
move_with_progress(source_files, target_directory)
|
|
361
|
+
|
|
362
|
+
return source_files
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymover
|
|
3
|
+
Version: 0.3.0.dev3
|
|
4
|
+
Summary: Custom move command with progress bar
|
|
5
|
+
Author: Thorbjoern Mumme
|
|
6
|
+
License: n.a.
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/thomumme/file-mover
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/thomumme/file-mover
|
|
9
|
+
Keywords: file,move,copy,progress
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: loguru
|
|
18
|
+
Requires-Dist: click
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: randomfiletree; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# FileMover
|
|
26
|
+
|
|
27
|
+
FileMover is a Python package that provides command-line tools for moving and copying files with progress bars. It supports both single file operations and batch operations on multiple files.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
You can install FileMover using pip:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install pymover
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install the latest development version from GitLab:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://gitlab.com/thomumme/file-mover.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Changelog
|
|
50
|
+
|
|
51
|
+
See [CHANGELOG.md](CHANGELOG.md) for a history of changes.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
pymover/__init__.py
|
|
6
|
+
pymover/__version__.py
|
|
7
|
+
pymover/cli.py
|
|
8
|
+
pymover/pymover.py
|
|
9
|
+
pymover/utils.py
|
|
10
|
+
pymover.egg-info/PKG-INFO
|
|
11
|
+
pymover.egg-info/SOURCES.txt
|
|
12
|
+
pymover.egg-info/dependency_links.txt
|
|
13
|
+
pymover.egg-info/requires.txt
|
|
14
|
+
pymover.egg-info/top_level.txt
|
|
15
|
+
tests/test_cli.py
|
|
16
|
+
tests/test_pymover.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymover
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 68.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pymover"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Custom move command with progress bar"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Thorbjoern Mumme"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "n.a."}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
keywords = ["file", "move", "copy", "progress"]
|
|
20
|
+
requires-python = ">=3.7"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"tqdm",
|
|
23
|
+
"loguru",
|
|
24
|
+
"click",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest",
|
|
30
|
+
"pytest-cov",
|
|
31
|
+
"randomfiletree",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
#[project.scripts]
|
|
35
|
+
#mmv = "filemover.cli:move"
|
|
36
|
+
#mcp = "filemover.cli:copy"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://gitlab.com/thomumme/file-mover"
|
|
40
|
+
Repository = "https://gitlab.com/thomumme/file-mover"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["."]
|
|
44
|
+
include = ["pymover*"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-dir]
|
|
47
|
+
pymover = "pymover"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.dynamic]
|
|
50
|
+
version = {attr = "pymover.__version__.__version__"}
|