fsync 1.0.0__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.
- fsync-1.0.0/PKG-INFO +110 -0
- fsync-1.0.0/fsync/__init__.py +31 -0
- fsync-1.0.0/fsync/__main__.py +4 -0
- fsync-1.0.0/fsync/args.py +19 -0
- fsync-1.0.0/fsync/diff.py +32 -0
- fsync-1.0.0/fsync/example.py +30 -0
- fsync-1.0.0/fsync/main.py +175 -0
- fsync-1.0.0/fsync/utils.py +36 -0
- fsync-1.0.0/fsync.egg-info/PKG-INFO +110 -0
- fsync-1.0.0/fsync.egg-info/SOURCES.txt +15 -0
- fsync-1.0.0/fsync.egg-info/dependency_links.txt +1 -0
- fsync-1.0.0/fsync.egg-info/entry_points.txt +2 -0
- fsync-1.0.0/fsync.egg-info/requires.txt +7 -0
- fsync-1.0.0/fsync.egg-info/top_level.txt +1 -0
- fsync-1.0.0/pyproject.toml +27 -0
- fsync-1.0.0/readme.md +94 -0
- fsync-1.0.0/setup.cfg +4 -0
fsync-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fsync
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Local file synchronization for scattered shared libraries
|
|
5
|
+
Author: Xaeian
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Xaeian/FSync
|
|
8
|
+
Keywords: sync,files,libraries,backup
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: xaeian
|
|
12
|
+
Provides-Extra: diff
|
|
13
|
+
Requires-Dist: rich; extra == "diff"
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: rich; extra == "all"
|
|
16
|
+
|
|
17
|
+
## 📖 FSync
|
|
18
|
+
|
|
19
|
+
This tool **eliminates the problem of scattered libraries** by synchronizing 🔄 selected files spread across different projects on your machine _(locally)_. No more chaos and manual file copying.
|
|
20
|
+
|
|
21
|
+
At first glance, centralizing libraries seems like a great idea. It makes work easier and avoids code duplication. In practice, however, some complications arise:
|
|
22
|
+
|
|
23
|
+
- You don't always want to update a library in a project you're no longer developing, but it still needs to work.
|
|
24
|
+
- Some libraries must be part of the repository. When you hand off/close a project, you want everything in one place, without having to pull additional dependencies from external sources.
|
|
25
|
+
- It's better when all resources are in the project directory. It simplifies configuration _(Makefile, CMake)_, eliminates path and version issues, and improves IDE integration.
|
|
26
|
+
|
|
27
|
+
This solution is perfect if you run many smaller projects and care about efficient library management _(code that appears across multiple projects)_. If you make frequent changes, want to avoid messy code, but don't have time to spend hours organizing dependencies: this tool is for you! The program is dead simple. What matters is efficient and effective work, without unnecessary bureaucracy. The open source community can do its thing, but here the priority is a happy client and a working project done ⚡**fast** and 👍**good enough**.
|
|
28
|
+
|
|
29
|
+
### 🧐 Problems!
|
|
30
|
+
|
|
31
|
+
- ❌ **Possible accidental overwrites**: if you edit two versions of a library at the same time.
|
|
32
|
+
- ✅ Avoid this, but if it happens, every overwritten version is saved as a timestamped backup, so you can always recover your changes.
|
|
33
|
+
- ❌ **No environment isolation**: different projects may require different versions of the same library.
|
|
34
|
+
- ✅ Not a problem! Just create separate entries for different versions, keeping synchronization independent. You can also comment out entries for libraries that shouldn't be updated anymore.
|
|
35
|
+
- ❌ **Code duplication across repositories**: instead of one library copy, you have several in different projects.
|
|
36
|
+
- ✅ That's the point! Each client should have their own library version, with no dependencies on other repos. Full control, zero unnecessary complications.
|
|
37
|
+
|
|
38
|
+
### 🤔 Alternatives?
|
|
39
|
+
|
|
40
|
+
Of course you can approach this more professionally by:
|
|
41
|
+
|
|
42
|
+
- Versioning libraries as separate projects/repositories and updating them as needed.
|
|
43
|
+
- Using Git **Submodules**, which allows tracking library versions in the repository.
|
|
44
|
+
- External package managers _(`pip`, `npm`, `cargo`)_ that simplify dependency management.
|
|
45
|
+
|
|
46
|
+
If any of our libraries reach a stable version that we don't chaotically change every project, and they're good enough, it's worth considering one of the above solutions.
|
|
47
|
+
|
|
48
|
+
### ⚙️ Config
|
|
49
|
+
|
|
50
|
+
The **`sync.json`** file defines file synchronization configuration. Each entry is a key _(filename)_ and a list of paths to synchronize. Keys starting with `#` are treated as commented out and skipped.
|
|
51
|
+
|
|
52
|
+
Paths can use shorthand notation via the **`dict.ini`** file, which defines aliases for frequently repeated locations. In `sync.json` paths you can reference these aliases using `{key}` notation.
|
|
53
|
+
|
|
54
|
+
#### Example
|
|
55
|
+
|
|
56
|
+
Running the program with the `-e`, `--example` flag will create example config files locally.
|
|
57
|
+
|
|
58
|
+
File `dict.ini`
|
|
59
|
+
|
|
60
|
+
```ini
|
|
61
|
+
web = C:/Users/Me/Projects/WebPage/backend
|
|
62
|
+
staff = C:/Users/Me/Desktop/MyStaff/test
|
|
63
|
+
work = C:/Users/Me/Work/Drivers/repos
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
File `sync.json`
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"serial.c": ["{staff}/serial.c", "{work}/PLC/serial_port.c"],
|
|
71
|
+
"utils.py": ["{web}/lib/utils.py", "{work}/PLC/misc.py"],
|
|
72
|
+
"#old_lib.c": ["{staff}/old_lib.c", "{work}/legacy/old_lib.c"]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 📦 Install
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pip install fsync # basic
|
|
80
|
+
pip install fsync[diff] # + rich (diff display)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Also available as standalone `.exe` on [GitHub Releases](https://github.com/Xaeian/FSync/releases).
|
|
84
|
+
|
|
85
|
+
### 🚀 Use
|
|
86
|
+
|
|
87
|
+
First, set the workspace: the directory containing your config files (`sync.json`, `dict.ini`) and where backups will be stored:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
fsync -w C:/Projects/sync # specified path
|
|
91
|
+
fsync -w # current directory
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Running the program generates a report:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
fsync
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
To synchronize _(i.e. update older file versions)_, just add the `-u`, `--update` flag:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
fsync -u
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For each pair of files with discrepancies, tags are generated. You can use them to inspect differences between files with the `-d`, `--diff` flag:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
fsync -d 1.1
|
|
110
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# fsync/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
FSync: local file synchronization tool.
|
|
5
|
+
|
|
6
|
+
Keeps scattered copies of shared libraries in sync across projects.
|
|
7
|
+
Works as CLI (`py -m fsync`), pip-installed command (`fsync`),
|
|
8
|
+
or importable module.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from fsync.main import main
|
|
12
|
+
>>> from fsync.utils import backup_file
|
|
13
|
+
>>> from fsync.diff import show
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
__repo__ = "Xaeian/FSync"
|
|
18
|
+
__python__ = ">=3.12"
|
|
19
|
+
__description__ = "Local file synchronization for scattered shared libraries"
|
|
20
|
+
__author__ = "Xaeian"
|
|
21
|
+
__keywords__ = ["sync", "files", "libraries", "backup"]
|
|
22
|
+
__dependencies__ = ["xaeian"]
|
|
23
|
+
__scripts__ = {
|
|
24
|
+
"fsync": "fsync.__main__:main",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
from .utils import backup_file, BACKUP_DIR, BACKUP_KEEP
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"backup_file", "BACKUP_DIR", "BACKUP_KEEP",
|
|
31
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# fsync/args.py
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
def load_args():
|
|
6
|
+
parser = argparse.ArgumentParser(description="FSync: File synchronization tool")
|
|
7
|
+
parser.add_argument("-w", "--workspace", type=str, nargs="?", const=".", default=None,
|
|
8
|
+
help="Set workspace directory (default: current dir)")
|
|
9
|
+
parser.add_argument("-u", "--update", action="store_true",
|
|
10
|
+
help="Update files to latest version (most recently modified)")
|
|
11
|
+
parser.add_argument("-i", "--info", action="store_true",
|
|
12
|
+
help="Display all synchronized files")
|
|
13
|
+
parser.add_argument("-e", "--example", action="store_true",
|
|
14
|
+
help="Create example config files")
|
|
15
|
+
parser.add_argument("-d", "--diff", type=str, nargs="?",
|
|
16
|
+
help="Compare files by tag: <latest>.<obsolete>", default="")
|
|
17
|
+
parser.add_argument("-v", "--version", action="store_true",
|
|
18
|
+
help="Program version and repository")
|
|
19
|
+
return parser.parse_args()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# fsync/diff.py
|
|
2
|
+
|
|
3
|
+
__extras__ = ("diff", ["rich"])
|
|
4
|
+
|
|
5
|
+
import sys, difflib
|
|
6
|
+
from xaeian import FILE, PATH, Print, Color as c, replace_map
|
|
7
|
+
|
|
8
|
+
def show(file_a:str, file_b:str, mapping:dict|None=None):
|
|
9
|
+
try:
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
except ImportError:
|
|
13
|
+
p = Print()
|
|
14
|
+
p.err(f"Missing {c.TURQUS}rich{c.END} package for diff display")
|
|
15
|
+
p.run(f"Install: {c.YELLOW}pip{c.END} install fsync[diff]")
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
lines_a = FILE.load_lines(file_a)
|
|
18
|
+
lines_b = FILE.load_lines(file_b)
|
|
19
|
+
if mapping:
|
|
20
|
+
label_a = replace_map(PATH.normalize(file_a), mapping)
|
|
21
|
+
label_b = replace_map(PATH.normalize(file_b), mapping)
|
|
22
|
+
else:
|
|
23
|
+
label_a, label_b = file_a, file_b
|
|
24
|
+
result = "".join(difflib.unified_diff(
|
|
25
|
+
lines_a, lines_b,
|
|
26
|
+
fromfile=label_a, tofile=label_b,
|
|
27
|
+
))
|
|
28
|
+
if result:
|
|
29
|
+
Console().print(Syntax(
|
|
30
|
+
result, "diff", theme="ansi_dark",
|
|
31
|
+
line_numbers=True, background_color=None,
|
|
32
|
+
))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# fsync/example.py
|
|
2
|
+
|
|
3
|
+
"""Generate example config files (dict.ini, sync.json)."""
|
|
4
|
+
|
|
5
|
+
import os, platform
|
|
6
|
+
from xaeian import JSON, INI, FILE, Print, Color as c
|
|
7
|
+
|
|
8
|
+
def create():
|
|
9
|
+
p = Print()
|
|
10
|
+
user = os.getlogin()
|
|
11
|
+
if platform.system() == "Windows": base = f"C:/Users/{user}"
|
|
12
|
+
else: base = f"/home/{user}"
|
|
13
|
+
mydict = {
|
|
14
|
+
"web": f"{base}/Projects/WebPage/backend",
|
|
15
|
+
"staff": f"{base}/Desktop/MyStaff/test",
|
|
16
|
+
"work": f"{base}/Work/Drivers/repos",
|
|
17
|
+
}
|
|
18
|
+
sync = {
|
|
19
|
+
"serial.c": ["{staff}/serial.c", "{work}/PLC/serial_port.c"],
|
|
20
|
+
"utils.py": ["{web}/lib/utils.py", "{work}/PLC/misc.py"],
|
|
21
|
+
}
|
|
22
|
+
for name, data, saver in [
|
|
23
|
+
("dict.ini", mydict, INI.save),
|
|
24
|
+
("sync.json", sync, JSON.save_pretty),
|
|
25
|
+
]:
|
|
26
|
+
if FILE.exists(name):
|
|
27
|
+
p.err(f"File {c.RED}{name}{c.END} already exists. Delete it to generate example")
|
|
28
|
+
else:
|
|
29
|
+
saver(name, data)
|
|
30
|
+
p.ok(f"Template {c.GREEN}{name}{c.END} generated")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# fsync/main.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
FSync: file synchronization engine.
|
|
5
|
+
|
|
6
|
+
Core sync logic: load workspace, resolve paths, detect
|
|
7
|
+
changes, update obsolete files with backup rotation.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from fsync.main import main
|
|
11
|
+
>>> main()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os, sys
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from xaeian import JSON, INI, FILE, DIR, PATH, replace_map, Ico, Print, Color as c
|
|
17
|
+
from xaeian.files import set_context
|
|
18
|
+
from .args import load_args
|
|
19
|
+
from . import __version__, __repo__
|
|
20
|
+
|
|
21
|
+
WORKSPACE_FILE = PATH.expand("~/.fsync.txt")
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
p = Print()
|
|
25
|
+
args = load_args()
|
|
26
|
+
|
|
27
|
+
if args.version:
|
|
28
|
+
print(f"FSync {c.BLUE}{__version__}{c.END}")
|
|
29
|
+
print(f"Repo: {c.GREY}https://{c.END}github.com/{__repo__}")
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
#------------------------------------------------------------------------------------- Workspace
|
|
33
|
+
if args.workspace is not None:
|
|
34
|
+
ws = os.path.abspath(PATH.expand(args.workspace))
|
|
35
|
+
if not PATH.is_dir(ws):
|
|
36
|
+
p.err(f"Directory {c.GREY}{ws}{c.END} doesn't exist")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
FILE.save(WORKSPACE_FILE, ws)
|
|
39
|
+
p.ok(f"Set directory {c.ORANGE}{ws}{c.END} as workspace")
|
|
40
|
+
if not any([args.update, args.info, args.example, args.diff]):
|
|
41
|
+
sys.exit(0)
|
|
42
|
+
|
|
43
|
+
if not PATH.is_file(WORKSPACE_FILE):
|
|
44
|
+
p.err(f"No workspace set. Use {c.GREY}-w --workspace{c.END} to set one")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
workspace = FILE.load(WORKSPACE_FILE).strip()
|
|
47
|
+
if not PATH.is_dir(workspace):
|
|
48
|
+
p.err(f"Workspace {c.ORANGE}{workspace}{c.END} doesn't exist")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
set_context(root_path=workspace)
|
|
52
|
+
|
|
53
|
+
if args.example:
|
|
54
|
+
from . import example
|
|
55
|
+
example.create()
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
#---------------------------------------------------------------------------------------- Config
|
|
59
|
+
|
|
60
|
+
sync = JSON.load("sync.json")
|
|
61
|
+
if not sync:
|
|
62
|
+
p.err(f"Missing or invalid {c.RED}sync.json{c.END}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
sync = {k: v for k, v in sync.items() if not k.startswith("#")}
|
|
65
|
+
if not sync:
|
|
66
|
+
p.err(f"No active entries in {c.RED}sync.json{c.END}")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
mydict = INI.load("dict.ini")
|
|
69
|
+
if not mydict:
|
|
70
|
+
p.err(f"Missing or invalid {c.RED}dict.ini{c.END}")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
#---------------------------------------------------------------------------------- Resolve paths
|
|
74
|
+
resolved = {}
|
|
75
|
+
for name, paths in sync.items():
|
|
76
|
+
resolved[name] = [replace_map(x, mydict, "{", "}") for x in paths]
|
|
77
|
+
|
|
78
|
+
#------------------------------------------------------------------------------------- Validate
|
|
79
|
+
errors = False
|
|
80
|
+
for name, paths in resolved.items():
|
|
81
|
+
seen = set()
|
|
82
|
+
for path in paths:
|
|
83
|
+
if not PATH.is_file(path):
|
|
84
|
+
p.err(f"Path {c.ORANGE}{path}{c.END} in {c.RED}{name}{c.END} doesn't exist")
|
|
85
|
+
errors = True
|
|
86
|
+
if path in seen:
|
|
87
|
+
p.err(f"Path {c.ORANGE}{path}{c.END} in {c.RED}{name}{c.END} duplicated")
|
|
88
|
+
errors = True
|
|
89
|
+
seen.add(path)
|
|
90
|
+
if errors:
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
if args.diff is None:
|
|
94
|
+
p.wrn(f"Flag {c.GREY}-d{c.END} requires tag, e.g. {c.GREY}-d {c.MAGNTA}1.1{c.END}")
|
|
95
|
+
args.diff = ""
|
|
96
|
+
|
|
97
|
+
#----------------------------------------------------------------------------------------- Diff
|
|
98
|
+
diff_ctx = {}
|
|
99
|
+
if args.diff:
|
|
100
|
+
diff_ctx["latest_nbr"], diff_ctx["obsolete_nbr"] = map(int, args.diff.split("."))
|
|
101
|
+
|
|
102
|
+
#----------------------------------------------------------------------------------------- Sync
|
|
103
|
+
update_flag = False
|
|
104
|
+
nbr_last = 0
|
|
105
|
+
|
|
106
|
+
def sync_file(name:str, paths:list[str]):
|
|
107
|
+
nonlocal update_flag, nbr_last
|
|
108
|
+
paths = [PATH.normalize(x) for x in paths]
|
|
109
|
+
paths = [x for x in paths if PATH.is_file(x)]
|
|
110
|
+
if len(paths) < 2: return
|
|
111
|
+
hashs = [FILE.hash(f, algo="md5") for f in paths]
|
|
112
|
+
stamps = [FILE.mtime(f) for f in paths]
|
|
113
|
+
dts = [datetime.fromtimestamp(s).strftime("%Y-%m-%d %H:%M:%S") for s in stamps]
|
|
114
|
+
if len(set(hashs)) == 1 and not args.info: return
|
|
115
|
+
latest_id = stamps.index(max(stamps))
|
|
116
|
+
latest_hash = hashs[latest_id]
|
|
117
|
+
latest_file = paths[latest_id]
|
|
118
|
+
latest_dt = dts[latest_id]
|
|
119
|
+
nbr_last += 1
|
|
120
|
+
nbr_obsolete = 0
|
|
121
|
+
if diff_ctx and diff_ctx.get("latest_nbr") == nbr_last:
|
|
122
|
+
diff_ctx["latest_file"] = latest_file
|
|
123
|
+
diff_ctx["name"] = name
|
|
124
|
+
p(f"{Ico.INF} {c.YELLOW}{nbr_last}{c.GREY}.x{c.END} Latest"
|
|
125
|
+
f" {c.BLUE}{name}{c.END}: {c.GREY}{latest_file}{c.END}"
|
|
126
|
+
f" {c.TEAL}{latest_dt}{c.END}")
|
|
127
|
+
for file, hash, dt, stamp in zip(paths, hashs, dts, stamps):
|
|
128
|
+
tag = lambda clr: f"{Ico.GAP} {clr}{nbr_last}.{nbr_obsolete}{c.END}"
|
|
129
|
+
if hash != latest_hash:
|
|
130
|
+
update_flag = True
|
|
131
|
+
nbr_obsolete += 1
|
|
132
|
+
color = c.YELLOW
|
|
133
|
+
if(diff_ctx and diff_ctx.get("latest_nbr") == nbr_last
|
|
134
|
+
and diff_ctx.get("obsolete_nbr") == nbr_obsolete):
|
|
135
|
+
diff_ctx["obsolete_file"] = file
|
|
136
|
+
color = c.GREEN
|
|
137
|
+
if args.update:
|
|
138
|
+
backed = utils.backup_file(file, name)
|
|
139
|
+
try:
|
|
140
|
+
DIR.copy(latest_file, file)
|
|
141
|
+
msg = f"{c.GREY}{file}{c.END} updated"
|
|
142
|
+
if backed: msg += f" {c.GREY}(backup){c.END}"
|
|
143
|
+
msg += f" {c.GREEN}OK{c.END}"
|
|
144
|
+
p(f"{tag(color)} {msg}")
|
|
145
|
+
except Exception:
|
|
146
|
+
p(f"{tag(color)} {c.GREY}{file}{c.END} update {c.RED}FAILED{c.END}")
|
|
147
|
+
else:
|
|
148
|
+
p(f"{tag(color)} Obsolete: {c.GREY}{file}{c.END} {c.ORANGE}{dt}{c.END}")
|
|
149
|
+
if stamp == os.path.getctime(file):
|
|
150
|
+
p.wrn("File was created recently, make sure it's not actually newer!")
|
|
151
|
+
elif (args.update or args.info) and file != latest_file:
|
|
152
|
+
p.ok(f"{c.GREY}{file}{c.END} is up-to-date")
|
|
153
|
+
|
|
154
|
+
from . import utils
|
|
155
|
+
|
|
156
|
+
for name, paths in resolved.items():
|
|
157
|
+
sync_file(name, paths)
|
|
158
|
+
|
|
159
|
+
if not update_flag:
|
|
160
|
+
p.inf(f"All files are in the same version {c.GREY}(no update needed){c.END}")
|
|
161
|
+
elif not args.update:
|
|
162
|
+
p.run(f"Update older files using {c.YELLOW}-u{c.END} {c.GREY}--update{c.END}")
|
|
163
|
+
p.run(f"Display file changes using {c.YELLOW}-d{c.END} {c.GREY}--diff{c.END}")
|
|
164
|
+
if args.diff:
|
|
165
|
+
if not diff_ctx or "obsolete_file" not in diff_ctx:
|
|
166
|
+
p.err(f"Invalid tag {c.GREEN}{args.diff}{c.END} for comparing files")
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
p.inf(f"Diff compare {c.BLUE}{diff_ctx['name']}{c.END}"
|
|
169
|
+
f" tag {c.GREEN}{diff_ctx['latest_nbr']}.{diff_ctx['obsolete_nbr']}{c.END}:")
|
|
170
|
+
reversed_dict = {PATH.normalize(v): f"{{{k}}}" for k, v in mydict.items()}
|
|
171
|
+
from . import diff
|
|
172
|
+
diff.show(diff_ctx["obsolete_file"], diff_ctx["latest_file"], reversed_dict)
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# fsync/utils.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from xaeian import FILE, DIR, PATH
|
|
6
|
+
|
|
7
|
+
BACKUP_DIR = "backups"
|
|
8
|
+
BACKUP_KEEP = 10
|
|
9
|
+
|
|
10
|
+
def _find_backups(name: str) -> list[str]:
|
|
11
|
+
"""Find all backups for given sync name, sorted oldest first."""
|
|
12
|
+
dir = PATH.resolve(f"{BACKUP_DIR}/{name}")
|
|
13
|
+
if not os.path.isdir(dir): return []
|
|
14
|
+
return [f"{dir}/{f}" for f in sorted(os.listdir(dir))]
|
|
15
|
+
|
|
16
|
+
def backup_file(path: str, name: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Backup file before overwriting.
|
|
19
|
+
Layout: backups/ary.c/2025-02-24@14-30-00.c
|
|
20
|
+
Skips if identical to latest backup. Rotates beyond BACKUP_KEEP.
|
|
21
|
+
"""
|
|
22
|
+
file_hash = FILE.hash(path, algo="md5")
|
|
23
|
+
existing = _find_backups(name)
|
|
24
|
+
if existing and FILE.hash(existing[-1], algo="md5") == file_hash:
|
|
25
|
+
return False
|
|
26
|
+
dir = PATH.resolve(f"{BACKUP_DIR}/{name}")
|
|
27
|
+
DIR.ensure(f"{dir}/")
|
|
28
|
+
ext = PATH.ext(name)
|
|
29
|
+
stamp = datetime.now().strftime("%Y-%m-%d@%H-%M-%S")
|
|
30
|
+
dst = f"{dir}/{stamp}{ext}"
|
|
31
|
+
DIR.copy(path, dst)
|
|
32
|
+
# Rotate
|
|
33
|
+
all_backups = _find_backups(name)
|
|
34
|
+
while len(all_backups) > BACKUP_KEEP:
|
|
35
|
+
os.remove(all_backups.pop(0))
|
|
36
|
+
return True
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fsync
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Local file synchronization for scattered shared libraries
|
|
5
|
+
Author: Xaeian
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Xaeian/FSync
|
|
8
|
+
Keywords: sync,files,libraries,backup
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: xaeian
|
|
12
|
+
Provides-Extra: diff
|
|
13
|
+
Requires-Dist: rich; extra == "diff"
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: rich; extra == "all"
|
|
16
|
+
|
|
17
|
+
## 📖 FSync
|
|
18
|
+
|
|
19
|
+
This tool **eliminates the problem of scattered libraries** by synchronizing 🔄 selected files spread across different projects on your machine _(locally)_. No more chaos and manual file copying.
|
|
20
|
+
|
|
21
|
+
At first glance, centralizing libraries seems like a great idea. It makes work easier and avoids code duplication. In practice, however, some complications arise:
|
|
22
|
+
|
|
23
|
+
- You don't always want to update a library in a project you're no longer developing, but it still needs to work.
|
|
24
|
+
- Some libraries must be part of the repository. When you hand off/close a project, you want everything in one place, without having to pull additional dependencies from external sources.
|
|
25
|
+
- It's better when all resources are in the project directory. It simplifies configuration _(Makefile, CMake)_, eliminates path and version issues, and improves IDE integration.
|
|
26
|
+
|
|
27
|
+
This solution is perfect if you run many smaller projects and care about efficient library management _(code that appears across multiple projects)_. If you make frequent changes, want to avoid messy code, but don't have time to spend hours organizing dependencies: this tool is for you! The program is dead simple. What matters is efficient and effective work, without unnecessary bureaucracy. The open source community can do its thing, but here the priority is a happy client and a working project done ⚡**fast** and 👍**good enough**.
|
|
28
|
+
|
|
29
|
+
### 🧐 Problems!
|
|
30
|
+
|
|
31
|
+
- ❌ **Possible accidental overwrites**: if you edit two versions of a library at the same time.
|
|
32
|
+
- ✅ Avoid this, but if it happens, every overwritten version is saved as a timestamped backup, so you can always recover your changes.
|
|
33
|
+
- ❌ **No environment isolation**: different projects may require different versions of the same library.
|
|
34
|
+
- ✅ Not a problem! Just create separate entries for different versions, keeping synchronization independent. You can also comment out entries for libraries that shouldn't be updated anymore.
|
|
35
|
+
- ❌ **Code duplication across repositories**: instead of one library copy, you have several in different projects.
|
|
36
|
+
- ✅ That's the point! Each client should have their own library version, with no dependencies on other repos. Full control, zero unnecessary complications.
|
|
37
|
+
|
|
38
|
+
### 🤔 Alternatives?
|
|
39
|
+
|
|
40
|
+
Of course you can approach this more professionally by:
|
|
41
|
+
|
|
42
|
+
- Versioning libraries as separate projects/repositories and updating them as needed.
|
|
43
|
+
- Using Git **Submodules**, which allows tracking library versions in the repository.
|
|
44
|
+
- External package managers _(`pip`, `npm`, `cargo`)_ that simplify dependency management.
|
|
45
|
+
|
|
46
|
+
If any of our libraries reach a stable version that we don't chaotically change every project, and they're good enough, it's worth considering one of the above solutions.
|
|
47
|
+
|
|
48
|
+
### ⚙️ Config
|
|
49
|
+
|
|
50
|
+
The **`sync.json`** file defines file synchronization configuration. Each entry is a key _(filename)_ and a list of paths to synchronize. Keys starting with `#` are treated as commented out and skipped.
|
|
51
|
+
|
|
52
|
+
Paths can use shorthand notation via the **`dict.ini`** file, which defines aliases for frequently repeated locations. In `sync.json` paths you can reference these aliases using `{key}` notation.
|
|
53
|
+
|
|
54
|
+
#### Example
|
|
55
|
+
|
|
56
|
+
Running the program with the `-e`, `--example` flag will create example config files locally.
|
|
57
|
+
|
|
58
|
+
File `dict.ini`
|
|
59
|
+
|
|
60
|
+
```ini
|
|
61
|
+
web = C:/Users/Me/Projects/WebPage/backend
|
|
62
|
+
staff = C:/Users/Me/Desktop/MyStaff/test
|
|
63
|
+
work = C:/Users/Me/Work/Drivers/repos
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
File `sync.json`
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"serial.c": ["{staff}/serial.c", "{work}/PLC/serial_port.c"],
|
|
71
|
+
"utils.py": ["{web}/lib/utils.py", "{work}/PLC/misc.py"],
|
|
72
|
+
"#old_lib.c": ["{staff}/old_lib.c", "{work}/legacy/old_lib.c"]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 📦 Install
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pip install fsync # basic
|
|
80
|
+
pip install fsync[diff] # + rich (diff display)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Also available as standalone `.exe` on [GitHub Releases](https://github.com/Xaeian/FSync/releases).
|
|
84
|
+
|
|
85
|
+
### 🚀 Use
|
|
86
|
+
|
|
87
|
+
First, set the workspace: the directory containing your config files (`sync.json`, `dict.ini`) and where backups will be stored:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
fsync -w C:/Projects/sync # specified path
|
|
91
|
+
fsync -w # current directory
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Running the program generates a report:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
fsync
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
To synchronize _(i.e. update older file versions)_, just add the `-u`, `--update` flag:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
fsync -u
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For each pair of files with discrepancies, tags are generated. You can use them to inspect differences between files with the `-d`, `--diff` flag:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
fsync -d 1.1
|
|
110
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
readme.md
|
|
3
|
+
fsync/__init__.py
|
|
4
|
+
fsync/__main__.py
|
|
5
|
+
fsync/args.py
|
|
6
|
+
fsync/diff.py
|
|
7
|
+
fsync/example.py
|
|
8
|
+
fsync/main.py
|
|
9
|
+
fsync/utils.py
|
|
10
|
+
fsync.egg-info/PKG-INFO
|
|
11
|
+
fsync.egg-info/SOURCES.txt
|
|
12
|
+
fsync.egg-info/dependency_links.txt
|
|
13
|
+
fsync.egg-info/entry_points.txt
|
|
14
|
+
fsync.egg-info/requires.txt
|
|
15
|
+
fsync.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fsync
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fsync"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Local file synchronization for scattered shared libraries"
|
|
9
|
+
readme = "readme.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [{name = "Xaeian"}]
|
|
13
|
+
keywords = ["sync", "files", "libraries", "backup"]
|
|
14
|
+
dependencies = ["xaeian"]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
diff = ["rich"]
|
|
18
|
+
all = ["rich"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
fsync = "fsync.__main__:main"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/Xaeian/FSync"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["fsync*"]
|
fsync-1.0.0/readme.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
## 📖 FSync
|
|
2
|
+
|
|
3
|
+
This tool **eliminates the problem of scattered libraries** by synchronizing 🔄 selected files spread across different projects on your machine _(locally)_. No more chaos and manual file copying.
|
|
4
|
+
|
|
5
|
+
At first glance, centralizing libraries seems like a great idea. It makes work easier and avoids code duplication. In practice, however, some complications arise:
|
|
6
|
+
|
|
7
|
+
- You don't always want to update a library in a project you're no longer developing, but it still needs to work.
|
|
8
|
+
- Some libraries must be part of the repository. When you hand off/close a project, you want everything in one place, without having to pull additional dependencies from external sources.
|
|
9
|
+
- It's better when all resources are in the project directory. It simplifies configuration _(Makefile, CMake)_, eliminates path and version issues, and improves IDE integration.
|
|
10
|
+
|
|
11
|
+
This solution is perfect if you run many smaller projects and care about efficient library management _(code that appears across multiple projects)_. If you make frequent changes, want to avoid messy code, but don't have time to spend hours organizing dependencies: this tool is for you! The program is dead simple. What matters is efficient and effective work, without unnecessary bureaucracy. The open source community can do its thing, but here the priority is a happy client and a working project done ⚡**fast** and 👍**good enough**.
|
|
12
|
+
|
|
13
|
+
### 🧐 Problems!
|
|
14
|
+
|
|
15
|
+
- ❌ **Possible accidental overwrites**: if you edit two versions of a library at the same time.
|
|
16
|
+
- ✅ Avoid this, but if it happens, every overwritten version is saved as a timestamped backup, so you can always recover your changes.
|
|
17
|
+
- ❌ **No environment isolation**: different projects may require different versions of the same library.
|
|
18
|
+
- ✅ Not a problem! Just create separate entries for different versions, keeping synchronization independent. You can also comment out entries for libraries that shouldn't be updated anymore.
|
|
19
|
+
- ❌ **Code duplication across repositories**: instead of one library copy, you have several in different projects.
|
|
20
|
+
- ✅ That's the point! Each client should have their own library version, with no dependencies on other repos. Full control, zero unnecessary complications.
|
|
21
|
+
|
|
22
|
+
### 🤔 Alternatives?
|
|
23
|
+
|
|
24
|
+
Of course you can approach this more professionally by:
|
|
25
|
+
|
|
26
|
+
- Versioning libraries as separate projects/repositories and updating them as needed.
|
|
27
|
+
- Using Git **Submodules**, which allows tracking library versions in the repository.
|
|
28
|
+
- External package managers _(`pip`, `npm`, `cargo`)_ that simplify dependency management.
|
|
29
|
+
|
|
30
|
+
If any of our libraries reach a stable version that we don't chaotically change every project, and they're good enough, it's worth considering one of the above solutions.
|
|
31
|
+
|
|
32
|
+
### ⚙️ Config
|
|
33
|
+
|
|
34
|
+
The **`sync.json`** file defines file synchronization configuration. Each entry is a key _(filename)_ and a list of paths to synchronize. Keys starting with `#` are treated as commented out and skipped.
|
|
35
|
+
|
|
36
|
+
Paths can use shorthand notation via the **`dict.ini`** file, which defines aliases for frequently repeated locations. In `sync.json` paths you can reference these aliases using `{key}` notation.
|
|
37
|
+
|
|
38
|
+
#### Example
|
|
39
|
+
|
|
40
|
+
Running the program with the `-e`, `--example` flag will create example config files locally.
|
|
41
|
+
|
|
42
|
+
File `dict.ini`
|
|
43
|
+
|
|
44
|
+
```ini
|
|
45
|
+
web = C:/Users/Me/Projects/WebPage/backend
|
|
46
|
+
staff = C:/Users/Me/Desktop/MyStaff/test
|
|
47
|
+
work = C:/Users/Me/Work/Drivers/repos
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
File `sync.json`
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"serial.c": ["{staff}/serial.c", "{work}/PLC/serial_port.c"],
|
|
55
|
+
"utils.py": ["{web}/lib/utils.py", "{work}/PLC/misc.py"],
|
|
56
|
+
"#old_lib.c": ["{staff}/old_lib.c", "{work}/legacy/old_lib.c"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 📦 Install
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pip install fsync # basic
|
|
64
|
+
pip install fsync[diff] # + rich (diff display)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> Also available as standalone `.exe` on [GitHub Releases](https://github.com/Xaeian/FSync/releases).
|
|
68
|
+
|
|
69
|
+
### 🚀 Use
|
|
70
|
+
|
|
71
|
+
First, set the workspace: the directory containing your config files (`sync.json`, `dict.ini`) and where backups will be stored:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
fsync -w C:/Projects/sync # specified path
|
|
75
|
+
fsync -w # current directory
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Running the program generates a report:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
fsync
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To synchronize _(i.e. update older file versions)_, just add the `-u`, `--update` flag:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
fsync -u
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For each pair of files with discrepancies, tags are generated. You can use them to inspect differences between files with the `-d`, `--diff` flag:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
fsync -d 1.1
|
|
94
|
+
```
|
fsync-1.0.0/setup.cfg
ADDED