fileflow-cli 0.1.0__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.
- fileflow/__init__.py +3 -0
- fileflow/__main__.py +6 -0
- fileflow/actions.py +164 -0
- fileflow/cli.py +193 -0
- fileflow/config.py +183 -0
- fileflow/engine.py +167 -0
- fileflow/matchers.py +214 -0
- fileflow/report.py +164 -0
- fileflow/rules.py +86 -0
- fileflow/templates.py +124 -0
- fileflow_cli-0.1.0.dist-info/METADATA +204 -0
- fileflow_cli-0.1.0.dist-info/RECORD +16 -0
- fileflow_cli-0.1.0.dist-info/WHEEL +5 -0
- fileflow_cli-0.1.0.dist-info/entry_points.txt +2 -0
- fileflow_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- fileflow_cli-0.1.0.dist-info/top_level.txt +1 -0
fileflow/__init__.py
ADDED
fileflow/__main__.py
ADDED
fileflow/actions.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""文件操作:移动、复制、删除等.
|
|
2
|
+
|
|
3
|
+
支持冲突策略:
|
|
4
|
+
- skip: 跳过目标已存在的文件
|
|
5
|
+
- rename: 自动添加后缀 (1), (2)...
|
|
6
|
+
- overwrite: 覆盖目标文件
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Callable
|
|
15
|
+
|
|
16
|
+
from .templates import resolve_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConflictStrategy(str, Enum):
|
|
20
|
+
"""文件冲突处理策略."""
|
|
21
|
+
|
|
22
|
+
SKIP = "skip"
|
|
23
|
+
RENAME = "rename"
|
|
24
|
+
OVERWRITE = "overwrite"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ActionResult:
|
|
28
|
+
"""一次文件操作的结果."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
source: Path,
|
|
33
|
+
target: Path | None = None,
|
|
34
|
+
success: bool = True,
|
|
35
|
+
action: str = "move",
|
|
36
|
+
skipped: bool = False,
|
|
37
|
+
error: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.source = source
|
|
40
|
+
self.target = target
|
|
41
|
+
self.success = success
|
|
42
|
+
self.action = action
|
|
43
|
+
self.skipped = skipped
|
|
44
|
+
self.error = error
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def summary(self) -> str:
|
|
48
|
+
if self.error:
|
|
49
|
+
return f"[red]✗ {self.action}: {self.source.name}[/] → {self.error}"
|
|
50
|
+
if self.skipped:
|
|
51
|
+
return f"[yellow]– 跳过: {self.source.name}[/] (目标已存在)"
|
|
52
|
+
return f"[green]✓ {self.action}: {self.source.name}[/] → {self.target}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_target_path(
|
|
56
|
+
target_template: str,
|
|
57
|
+
source: Path,
|
|
58
|
+
base_dir: Path,
|
|
59
|
+
conflict: ConflictStrategy = ConflictStrategy.SKIP,
|
|
60
|
+
) -> Path:
|
|
61
|
+
"""解析目标路径,并根据冲突策略处理.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
最终的目标路径(可能已根据冲突策略调整).
|
|
65
|
+
"""
|
|
66
|
+
dest = resolve_path(target_template, source, base_dir)
|
|
67
|
+
|
|
68
|
+
if conflict == ConflictStrategy.RENAME and dest.exists():
|
|
69
|
+
dest = _find_available_name(dest)
|
|
70
|
+
|
|
71
|
+
return dest
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _find_available_name(dest: Path) -> Path:
|
|
75
|
+
"""找到一个不存在的路径,如 file (1).txt, file (2).txt."""
|
|
76
|
+
parent = dest.parent
|
|
77
|
+
stem = dest.stem
|
|
78
|
+
suffix = dest.suffix
|
|
79
|
+
counter = 1
|
|
80
|
+
while True:
|
|
81
|
+
new_name = f"{stem} ({counter}){suffix}"
|
|
82
|
+
candidate = parent / new_name
|
|
83
|
+
if not candidate.exists():
|
|
84
|
+
return candidate
|
|
85
|
+
counter += 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def move_file(
|
|
89
|
+
source: Path,
|
|
90
|
+
target_template: str,
|
|
91
|
+
base_dir: Path,
|
|
92
|
+
create_dirs: bool = True,
|
|
93
|
+
conflict: ConflictStrategy = ConflictStrategy.SKIP,
|
|
94
|
+
) -> ActionResult:
|
|
95
|
+
"""移动文件."""
|
|
96
|
+
try:
|
|
97
|
+
dest = resolve_target_path(target_template, source, base_dir, conflict)
|
|
98
|
+
if dest == source:
|
|
99
|
+
return ActionResult(source, dest, success=True, action="move")
|
|
100
|
+
|
|
101
|
+
if dest.exists() and conflict == ConflictStrategy.SKIP:
|
|
102
|
+
return ActionResult(source, dest, skipped=True, action="move")
|
|
103
|
+
|
|
104
|
+
if create_dirs:
|
|
105
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
shutil.move(str(source), str(dest))
|
|
108
|
+
return ActionResult(source, dest, success=True, action="move")
|
|
109
|
+
except OSError as e:
|
|
110
|
+
return ActionResult(source, error=str(e), success=False, action="move")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def copy_file(
|
|
114
|
+
source: Path,
|
|
115
|
+
target_template: str,
|
|
116
|
+
base_dir: Path,
|
|
117
|
+
create_dirs: bool = True,
|
|
118
|
+
conflict: ConflictStrategy = ConflictStrategy.SKIP,
|
|
119
|
+
) -> ActionResult:
|
|
120
|
+
"""复制文件."""
|
|
121
|
+
try:
|
|
122
|
+
dest = resolve_target_path(target_template, source, base_dir, conflict)
|
|
123
|
+
if dest.exists() and conflict == ConflictStrategy.SKIP:
|
|
124
|
+
return ActionResult(source, dest, skipped=True, action="copy")
|
|
125
|
+
|
|
126
|
+
if create_dirs:
|
|
127
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
shutil.copy2(str(source), str(dest))
|
|
130
|
+
return ActionResult(source, dest, success=True, action="copy")
|
|
131
|
+
except OSError as e:
|
|
132
|
+
return ActionResult(source, error=str(e), success=False, action="copy")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def delete_file(source: Path) -> ActionResult:
|
|
136
|
+
"""删除文件."""
|
|
137
|
+
try:
|
|
138
|
+
source.unlink()
|
|
139
|
+
return ActionResult(source, success=True, action="delete")
|
|
140
|
+
except OSError as e:
|
|
141
|
+
return ActionResult(source, error=str(e), success=False, action="delete")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── 操作注册表 ─────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
ActionFunc = Callable[..., ActionResult]
|
|
147
|
+
|
|
148
|
+
_actions: dict[str, ActionFunc] = {
|
|
149
|
+
"move": move_file,
|
|
150
|
+
"copy": copy_file,
|
|
151
|
+
"delete": delete_file,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_action(name: str) -> ActionFunc:
|
|
156
|
+
"""按名称获取操作函数."""
|
|
157
|
+
if name not in _actions:
|
|
158
|
+
raise KeyError(f"未知操作: {name!r},可选: {list(_actions.keys())}")
|
|
159
|
+
return _actions[name]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def list_actions() -> list[str]:
|
|
163
|
+
"""列出所有注册的操作名称."""
|
|
164
|
+
return list(_actions.keys())
|
fileflow/cli.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""FileFlow CLI 命令入口.
|
|
2
|
+
|
|
3
|
+
使用 Typer 构建,支持自动补全和 --help。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich import print as rprint
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .config import (
|
|
16
|
+
ConfigError,
|
|
17
|
+
find_config,
|
|
18
|
+
generate_default_config,
|
|
19
|
+
load_config,
|
|
20
|
+
)
|
|
21
|
+
from .engine import run
|
|
22
|
+
from .report import (
|
|
23
|
+
print_banner,
|
|
24
|
+
print_config_preview,
|
|
25
|
+
print_rule_list,
|
|
26
|
+
console,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="fileflow",
|
|
31
|
+
help="🧹 智能文件整理器 — 基于 YAML 规则引擎自动分类整理文件",
|
|
32
|
+
add_completion=True,
|
|
33
|
+
pretty_exceptions_show_locals=False,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
if value:
|
|
39
|
+
rprint(f"[bold cyan]FileFlow[/] [yellow]v{__version__}[/]")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def main(
|
|
45
|
+
version: bool = typer.Option(
|
|
46
|
+
False, "--version", "-V",
|
|
47
|
+
help="显示版本号并退出",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""FileFlow 智能文件整理器."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command(name="run")
|
|
57
|
+
def run_command(
|
|
58
|
+
source_dir: str = typer.Argument(
|
|
59
|
+
".", help="要整理的目录路径(默认当前目录)",
|
|
60
|
+
),
|
|
61
|
+
config: Optional[str] = typer.Option(
|
|
62
|
+
None, "--config", "-c",
|
|
63
|
+
help="配置文件路径(默认自动搜索 .fileflow.yaml)",
|
|
64
|
+
),
|
|
65
|
+
force: bool = typer.Option(
|
|
66
|
+
False, "--force", "-f",
|
|
67
|
+
help="跳过 dry-run 确认,直接执行",
|
|
68
|
+
),
|
|
69
|
+
dry_run: bool = typer.Option(
|
|
70
|
+
False, "--dry-run", "-n",
|
|
71
|
+
help="仅预览不执行(覆盖配置中的 dry_run_by_default)",
|
|
72
|
+
),
|
|
73
|
+
yes: bool = typer.Option(
|
|
74
|
+
False, "--yes", "-y",
|
|
75
|
+
help="自动确认,跳过交互提示",
|
|
76
|
+
),
|
|
77
|
+
no_recursive: bool = typer.Option(
|
|
78
|
+
False, "--no-recursive",
|
|
79
|
+
help="不递归扫描子目录",
|
|
80
|
+
),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""运行文件整理(默认先预览)."""
|
|
83
|
+
print_banner()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
config_path = Path(config) if config else None
|
|
87
|
+
is_dry_run = dry_run or None # None 表示使用配置默认值
|
|
88
|
+
if force:
|
|
89
|
+
is_dry_run = False
|
|
90
|
+
|
|
91
|
+
result = run(
|
|
92
|
+
source_dir=source_dir,
|
|
93
|
+
config_path=config_path,
|
|
94
|
+
dry_run=is_dry_run,
|
|
95
|
+
force=force,
|
|
96
|
+
recursive=not no_recursive,
|
|
97
|
+
)
|
|
98
|
+
except ConfigError as e:
|
|
99
|
+
console.print(f"[red]配置错误:[/] {e}")
|
|
100
|
+
raise typer.Exit(code=1)
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
console.print(f"[red]错误:[/] {e}")
|
|
103
|
+
raise typer.Exit(code=1)
|
|
104
|
+
|
|
105
|
+
result.print()
|
|
106
|
+
|
|
107
|
+
# Dry-run 模式下询问是否要执行
|
|
108
|
+
if result.dry_run and result.results and not yes:
|
|
109
|
+
proceed = typer.confirm(
|
|
110
|
+
"\n[bold]是否要执行以上操作?[/]",
|
|
111
|
+
default=False,
|
|
112
|
+
)
|
|
113
|
+
if proceed:
|
|
114
|
+
result = run(
|
|
115
|
+
source_dir=source_dir,
|
|
116
|
+
config_path=config_path,
|
|
117
|
+
dry_run=False,
|
|
118
|
+
force=False,
|
|
119
|
+
recursive=not no_recursive,
|
|
120
|
+
)
|
|
121
|
+
result.print()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def init(
|
|
126
|
+
global_config: bool = typer.Option(
|
|
127
|
+
False, "--global", "-g",
|
|
128
|
+
help="在当前目录生成默认配置",
|
|
129
|
+
),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""在当前目录生成默认配置文件 .fileflow.yaml."""
|
|
132
|
+
target = Path.cwd() / ".fileflow.yaml"
|
|
133
|
+
if target.exists():
|
|
134
|
+
overwrite = typer.confirm(
|
|
135
|
+
f"[yellow]{target} 已存在,是否覆盖?[/]",
|
|
136
|
+
default=False,
|
|
137
|
+
)
|
|
138
|
+
if not overwrite:
|
|
139
|
+
console.print("[yellow]已取消[/]")
|
|
140
|
+
raise typer.Exit()
|
|
141
|
+
|
|
142
|
+
content = generate_default_config()
|
|
143
|
+
target.write_text(content, encoding="utf-8")
|
|
144
|
+
console.print(f"[green]✅ 已生成配置文件:[/] {target}")
|
|
145
|
+
console.print("\n[dim]现在你可以:\n 1. 编辑 {target.name} 自定义规则\n 2. 运行 [bold]fileflow run .[/bold] 预览效果\n 3. 加 [bold]--force[/bold] 执行整理[/dim]")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def validate(
|
|
150
|
+
config: Optional[str] = typer.Option(
|
|
151
|
+
None, "--config", "-c",
|
|
152
|
+
help="配置文件路径",
|
|
153
|
+
),
|
|
154
|
+
) -> None:
|
|
155
|
+
"""验证配置文件语法和规则."""
|
|
156
|
+
try:
|
|
157
|
+
config_path = Path(config) if config else None
|
|
158
|
+
data = load_config(config_path)
|
|
159
|
+
console.print("[green]✅ 配置文件验证通过[/]")
|
|
160
|
+
print_config_preview(data)
|
|
161
|
+
except ConfigError as e:
|
|
162
|
+
console.print(f"[red]❌ 配置验证失败:[/] {e}")
|
|
163
|
+
raise typer.Exit(code=1)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command()
|
|
167
|
+
def list_rules(
|
|
168
|
+
verbose: bool = typer.Option(
|
|
169
|
+
False, "--verbose", "-v",
|
|
170
|
+
help="显示详细的匹配条件",
|
|
171
|
+
),
|
|
172
|
+
config: Optional[str] = typer.Option(
|
|
173
|
+
None, "--config", "-c",
|
|
174
|
+
help="配置文件路径",
|
|
175
|
+
),
|
|
176
|
+
) -> None:
|
|
177
|
+
"""列出当前配置中的所有规则."""
|
|
178
|
+
try:
|
|
179
|
+
config_path = Path(config) if config else None
|
|
180
|
+
data = load_config(config_path)
|
|
181
|
+
rules = data.get("rules", [])
|
|
182
|
+
if not rules:
|
|
183
|
+
console.print("[yellow]⚠ 配置中没有定义任何规则[/]")
|
|
184
|
+
raise typer.Exit()
|
|
185
|
+
|
|
186
|
+
print_rule_list(rules, verbose=verbose)
|
|
187
|
+
except ConfigError as e:
|
|
188
|
+
console.print(f"[red]错误:[/] {e}")
|
|
189
|
+
raise typer.Exit(code=1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
app()
|
fileflow/config.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""配置文件加载、解析和验证."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .rules import RuleSet
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONFIG = {
|
|
14
|
+
"version": "1.0",
|
|
15
|
+
"settings": {
|
|
16
|
+
"history_file": ".fileflow_history.json",
|
|
17
|
+
"dry_run_by_default": True,
|
|
18
|
+
"create_target_dirs": True,
|
|
19
|
+
"on_conflict": "skip",
|
|
20
|
+
},
|
|
21
|
+
"rules": [],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigError(Exception):
|
|
26
|
+
"""配置相关错误."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_config(path: Path | str | None = None) -> Path | None:
|
|
30
|
+
"""查找配置文件。
|
|
31
|
+
|
|
32
|
+
搜索顺序:
|
|
33
|
+
1. 用户指定的路径
|
|
34
|
+
2. 当前目录下的 .fileflow.yaml
|
|
35
|
+
3. 当前目录下的 .fileflow.yml
|
|
36
|
+
"""
|
|
37
|
+
if path:
|
|
38
|
+
p = Path(path)
|
|
39
|
+
if p.exists():
|
|
40
|
+
return p.resolve()
|
|
41
|
+
raise ConfigError(f"配置文件不存在: {path}")
|
|
42
|
+
|
|
43
|
+
cwd = Path.cwd()
|
|
44
|
+
for name in (".fileflow.yaml", ".fileflow.yml"):
|
|
45
|
+
candidate = cwd / name
|
|
46
|
+
if candidate.exists():
|
|
47
|
+
return candidate.resolve()
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_config(path: Path | str | None = None) -> dict[str, Any]:
|
|
53
|
+
"""加载 YAML 配置文件.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
path: 配置文件路径,为 None 时自动搜索.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
完整的配置字典(合并了默认值).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ConfigError: 文件不存在或格式错误.
|
|
63
|
+
"""
|
|
64
|
+
resolved = find_config(path)
|
|
65
|
+
if resolved is None:
|
|
66
|
+
msg = (
|
|
67
|
+
"未找到 .fileflow.yaml 配置文件。\n"
|
|
68
|
+
"请先在目标目录运行: fileflow init"
|
|
69
|
+
)
|
|
70
|
+
raise ConfigError(msg)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
raw = resolved.read_text(encoding="utf-8")
|
|
74
|
+
data: dict[str, Any] = yaml.safe_load(raw) or {}
|
|
75
|
+
except yaml.YAMLError as e:
|
|
76
|
+
raise ConfigError(f"YAML 解析错误: {e}") from e
|
|
77
|
+
except OSError as e:
|
|
78
|
+
raise ConfigError(f"读取文件失败: {e}") from e
|
|
79
|
+
|
|
80
|
+
config = _merge_defaults(data)
|
|
81
|
+
validate_config(config)
|
|
82
|
+
return config
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _merge_defaults(data: dict[str, Any]) -> dict[str, Any]:
|
|
86
|
+
"""将用户配置与默认值合并."""
|
|
87
|
+
merged = DEFAULT_CONFIG.copy()
|
|
88
|
+
merged.update(data)
|
|
89
|
+
|
|
90
|
+
# 合并 settings
|
|
91
|
+
if "settings" in data and isinstance(data["settings"], dict):
|
|
92
|
+
merged_settings = merged["settings"].copy()
|
|
93
|
+
merged_settings.update({
|
|
94
|
+
k: v for k, v in data["settings"].items()
|
|
95
|
+
if v is not None
|
|
96
|
+
})
|
|
97
|
+
merged["settings"] = merged_settings
|
|
98
|
+
|
|
99
|
+
return merged
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_config(config: dict[str, Any]) -> None:
|
|
103
|
+
"""验证配置合法性."""
|
|
104
|
+
settings = config.get("settings", {})
|
|
105
|
+
valid_conflicts = {"skip", "rename", "overwrite", "ask"}
|
|
106
|
+
on_conflict = settings.get("on_conflict", "skip")
|
|
107
|
+
if on_conflict not in valid_conflicts:
|
|
108
|
+
raise ConfigError(
|
|
109
|
+
f"无效的 on_conflict 值: {on_conflict!r}。"
|
|
110
|
+
f" 可选: {', '.join(valid_conflicts)}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
rules = config.get("rules", [])
|
|
114
|
+
if not isinstance(rules, list):
|
|
115
|
+
raise ConfigError("'rules' 必须是列表")
|
|
116
|
+
|
|
117
|
+
for i, rule in enumerate(rules):
|
|
118
|
+
if not isinstance(rule, dict):
|
|
119
|
+
raise ConfigError(f"rules[{i}] 必须是对象")
|
|
120
|
+
if "name" not in rule:
|
|
121
|
+
raise ConfigError(f"rules[{i}] 缺少 'name' 字段")
|
|
122
|
+
if "match" not in rule:
|
|
123
|
+
raise ConfigError(f"规则 '{rule['name']}' 缺少 'match' 字段")
|
|
124
|
+
if "target" not in rule and rule.get("action", "move") in ("move", "copy"):
|
|
125
|
+
raise ConfigError(f"规则 '{rule['name']}' 缺少 'target' 路径")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def generate_default_config() -> str:
|
|
129
|
+
"""生成默认配置文件内容."""
|
|
130
|
+
return """# FileFlow 配置文件
|
|
131
|
+
# 运行 `fileflow run` 会自动读取此文件
|
|
132
|
+
|
|
133
|
+
version: "1.0"
|
|
134
|
+
|
|
135
|
+
settings:
|
|
136
|
+
history_file: ".fileflow_history.json"
|
|
137
|
+
dry_run_by_default: true
|
|
138
|
+
create_target_dirs: true
|
|
139
|
+
on_conflict: "skip" # skip | rename | overwrite | ask
|
|
140
|
+
|
|
141
|
+
rules:
|
|
142
|
+
- name: "整理图片"
|
|
143
|
+
description: "按扩展名整理图片文件"
|
|
144
|
+
match:
|
|
145
|
+
extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"]
|
|
146
|
+
action: move
|
|
147
|
+
target: "Pictures/{ext_no_dot}/{filename}"
|
|
148
|
+
|
|
149
|
+
- name: "整理文档"
|
|
150
|
+
description: "归类 Office 和 PDF 文件"
|
|
151
|
+
match:
|
|
152
|
+
extensions: [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]
|
|
153
|
+
action: move
|
|
154
|
+
target: "Documents/{ext_no_dot}/{filename}"
|
|
155
|
+
|
|
156
|
+
- name: "整理视频"
|
|
157
|
+
description: "归类视频文件"
|
|
158
|
+
match:
|
|
159
|
+
extensions: [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv"]
|
|
160
|
+
action: move
|
|
161
|
+
target: "Videos/{ext_no_dot}/{filename}"
|
|
162
|
+
|
|
163
|
+
- name: "整理音频"
|
|
164
|
+
description: "归类音频文件"
|
|
165
|
+
match:
|
|
166
|
+
extensions: [".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma"]
|
|
167
|
+
action: move
|
|
168
|
+
target: "Audio/{ext_no_dot}/{filename}"
|
|
169
|
+
|
|
170
|
+
- name: "整理压缩包"
|
|
171
|
+
description: "归类压缩文件"
|
|
172
|
+
match:
|
|
173
|
+
extensions: [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"]
|
|
174
|
+
action: move
|
|
175
|
+
target: "Archives/{ext_no_dot}/{filename}"
|
|
176
|
+
|
|
177
|
+
- name: "整理代码文件"
|
|
178
|
+
description: "归类代码和文本文件"
|
|
179
|
+
match:
|
|
180
|
+
extensions: [".py", ".js", ".ts", ".html", ".css", ".json", ".xml", ".md", ".txt"]
|
|
181
|
+
action: move
|
|
182
|
+
target: "Code/{ext_no_dot}/{filename}"
|
|
183
|
+
"""
|