snapshot-cli 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.
- snapshot_cli-1.0.0/.gitignore +23 -0
- snapshot_cli-1.0.0/LICENSE +21 -0
- snapshot_cli-1.0.0/PKG-INFO +146 -0
- snapshot_cli-1.0.0/README.md +119 -0
- snapshot_cli-1.0.0/pyproject.toml +44 -0
- snapshot_cli-1.0.0/src/snapshot/__init__.py +3 -0
- snapshot_cli-1.0.0/src/snapshot/__main__.py +5 -0
- snapshot_cli-1.0.0/src/snapshot/cli.py +75 -0
- snapshot_cli-1.0.0/src/snapshot/commands/__init__.py +1 -0
- snapshot_cli-1.0.0/src/snapshot/commands/delete_snap.py +64 -0
- snapshot_cli-1.0.0/src/snapshot/commands/info_snap.py +44 -0
- snapshot_cli-1.0.0/src/snapshot/commands/init.py +17 -0
- snapshot_cli-1.0.0/src/snapshot/commands/list_snap.py +58 -0
- snapshot_cli-1.0.0/src/snapshot/commands/restore_latest.py +21 -0
- snapshot_cli-1.0.0/src/snapshot/commands/restore_snap.py +73 -0
- snapshot_cli-1.0.0/src/snapshot/commands/save_snap.py +36 -0
- snapshot_cli-1.0.0/src/snapshot/commands/stats_snap.py +83 -0
- snapshot_cli-1.0.0/src/snapshot/config.py +8 -0
- snapshot_cli-1.0.0/src/snapshot/utils/__init__.py +1 -0
- snapshot_cli-1.0.0/src/snapshot/utils/console.py +102 -0
- snapshot_cli-1.0.0/src/snapshot/utils/file_manager.py +62 -0
- snapshot_cli-1.0.0/src/snapshot/utils/metadata_loader.py +11 -0
- snapshot_cli-1.0.0/src/snapshot/utils/snapignore.py +36 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Virtual environments
|
|
2
|
+
/env/
|
|
3
|
+
/test_env/
|
|
4
|
+
.venv/
|
|
5
|
+
|
|
6
|
+
# Snap data
|
|
7
|
+
.snapignore
|
|
8
|
+
/.snap/
|
|
9
|
+
|
|
10
|
+
# Python bytecode
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
|
|
15
|
+
# Distribution / packaging
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
*.egg
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sparkleeop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapshot-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight project snapshot & versioning tool for developers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sparkleeop/snapshot
|
|
6
|
+
Project-URL: Repository, https://github.com/Sparkleeop/snapshot
|
|
7
|
+
Project-URL: Issues, https://github.com/Sparkleeop/snapshot/issues
|
|
8
|
+
Author: Sparkleeop
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backup,cli,developer-tools,snapshot,versioning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
23
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Snapshot
|
|
29
|
+
|
|
30
|
+
Snapshot is a lightweight code snapshotting tool that allows you to save and restore your entire project at any point in time.
|
|
31
|
+
|
|
32
|
+
Think of it as a simpler alternative to Git when you just want quick save points without branches, commits, remotes, or repositories.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
* Save snapshots of your entire project
|
|
37
|
+
* Restore previous snapshots at any time
|
|
38
|
+
* Ignore files and directories using `.snapignore`
|
|
39
|
+
* View snapshot history
|
|
40
|
+
* View detailed snapshot information
|
|
41
|
+
* Clean restore mode for exact project restoration
|
|
42
|
+
* Rich terminal interface
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### From PyPI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install snapshot-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### From source (development)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/Sparkleeop/snapshot.git
|
|
56
|
+
cd snapshot
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This installs the `snap` command globally in your environment.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
Initialize Snapshot in your project root:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
snap init
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Create a snapshot:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
snap save "before authentication rewrite"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
List all snapshots, you can also find <snap id> here:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
snap list
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
View information about a snapshot:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
snap info <snap id>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restore a snapshot:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
snap restore <snap id>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Perform a clean restore (removes existing project files before restoring):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
snap restore <snap id> --clean
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Perform a restore to the latest snap:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
snap restorelatest <--clean>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Delete a snapshot:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
snap del <snap id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Get the overall project statistics:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
snap stats
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## .snapignore
|
|
119
|
+
|
|
120
|
+
Use a `.snapignore` file in your project root to exclude files and directories from snapshots.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
.env
|
|
126
|
+
venv
|
|
127
|
+
__pycache__
|
|
128
|
+
node_modules
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Publishing to PyPI
|
|
132
|
+
|
|
133
|
+
Build and publish:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install build twine
|
|
137
|
+
python -m build
|
|
138
|
+
twine upload dist/*
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Roadmap
|
|
142
|
+
|
|
143
|
+
* [ ] Compare snapshots (`snap diff <id1> <id2>`)
|
|
144
|
+
* [ ] Snapshot export/import
|
|
145
|
+
* [ ] Automatic snapshots
|
|
146
|
+
* [ ] Head snapshot
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Snapshot
|
|
2
|
+
|
|
3
|
+
Snapshot is a lightweight code snapshotting tool that allows you to save and restore your entire project at any point in time.
|
|
4
|
+
|
|
5
|
+
Think of it as a simpler alternative to Git when you just want quick save points without branches, commits, remotes, or repositories.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Save snapshots of your entire project
|
|
10
|
+
* Restore previous snapshots at any time
|
|
11
|
+
* Ignore files and directories using `.snapignore`
|
|
12
|
+
* View snapshot history
|
|
13
|
+
* View detailed snapshot information
|
|
14
|
+
* Clean restore mode for exact project restoration
|
|
15
|
+
* Rich terminal interface
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### From PyPI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install snapshot-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From source (development)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/Sparkleeop/snapshot.git
|
|
29
|
+
cd snapshot
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This installs the `snap` command globally in your environment.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Initialize Snapshot in your project root:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
snap init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Create a snapshot:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
snap save "before authentication rewrite"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
List all snapshots, you can also find <snap id> here:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
snap list
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
View information about a snapshot:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
snap info <snap id>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Restore a snapshot:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
snap restore <snap id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Perform a clean restore (removes existing project files before restoring):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
snap restore <snap id> --clean
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Perform a restore to the latest snap:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
snap restorelatest <--clean>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Delete a snapshot:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
snap del <snap id>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Get the overall project statistics:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
snap stats
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## .snapignore
|
|
92
|
+
|
|
93
|
+
Use a `.snapignore` file in your project root to exclude files and directories from snapshots.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
.env
|
|
99
|
+
venv
|
|
100
|
+
__pycache__
|
|
101
|
+
node_modules
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Publishing to PyPI
|
|
105
|
+
|
|
106
|
+
Build and publish:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install build twine
|
|
110
|
+
python -m build
|
|
111
|
+
twine upload dist/*
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Roadmap
|
|
115
|
+
|
|
116
|
+
* [ ] Compare snapshots (`snap diff <id1> <id2>`)
|
|
117
|
+
* [ ] Snapshot export/import
|
|
118
|
+
* [ ] Automatic snapshots
|
|
119
|
+
* [ ] Head snapshot
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snapshot-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A lightweight project snapshot & versioning tool for developers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sparkleeop" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["snapshot", "versioning", "backup", "cli", "developer-tools"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Version Control",
|
|
28
|
+
"Topic :: System :: Archiving :: Backup",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"rich>=13.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Sparkleeop/snapshot"
|
|
37
|
+
Repository = "https://github.com/Sparkleeop/snapshot"
|
|
38
|
+
Issues = "https://github.com/Sparkleeop/snapshot/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
snap = "snapshot.cli:main"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/snapshot"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from snapshot.commands.init import init
|
|
4
|
+
from snapshot.commands.list_snap import list_snap
|
|
5
|
+
from snapshot.commands.save_snap import snapshot
|
|
6
|
+
from snapshot.commands.restore_snap import restore
|
|
7
|
+
from snapshot.commands.restore_latest import restore_latest
|
|
8
|
+
from snapshot.commands.info_snap import show_info
|
|
9
|
+
from snapshot.commands.delete_snap import delete
|
|
10
|
+
from snapshot.commands.stats_snap import stats
|
|
11
|
+
|
|
12
|
+
from snapshot.utils.console import print_help
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="snap",
|
|
18
|
+
description="Project snapshot manager",
|
|
19
|
+
add_help=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
23
|
+
|
|
24
|
+
subparsers.add_parser("init")
|
|
25
|
+
subparsers.add_parser("list")
|
|
26
|
+
subparsers.add_parser("stats")
|
|
27
|
+
|
|
28
|
+
save_parser = subparsers.add_parser("save")
|
|
29
|
+
save_parser.add_argument("message")
|
|
30
|
+
|
|
31
|
+
restore_parser = subparsers.add_parser("restore")
|
|
32
|
+
restore_parser.add_argument("snap_id", type=int)
|
|
33
|
+
restore_parser.add_argument("--clean", action="store_true")
|
|
34
|
+
|
|
35
|
+
restore_latest_parser = subparsers.add_parser("restorelatest")
|
|
36
|
+
restore_latest_parser.add_argument("--clean", action="store_true")
|
|
37
|
+
|
|
38
|
+
info_parser = subparsers.add_parser("info")
|
|
39
|
+
info_parser.add_argument("snap_id", type=int)
|
|
40
|
+
|
|
41
|
+
del_parser = subparsers.add_parser("del")
|
|
42
|
+
del_parser.add_argument("snap_id", type=int)
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
if args.command == "init":
|
|
47
|
+
init()
|
|
48
|
+
|
|
49
|
+
elif args.command == "save":
|
|
50
|
+
snapshot(args.message)
|
|
51
|
+
|
|
52
|
+
elif args.command == "restore":
|
|
53
|
+
restore(args.snap_id, clean=args.clean)
|
|
54
|
+
|
|
55
|
+
elif args.command == "restorelatest":
|
|
56
|
+
restore_latest(is_clean=args.clean)
|
|
57
|
+
|
|
58
|
+
elif args.command == "del":
|
|
59
|
+
delete(args.snap_id)
|
|
60
|
+
|
|
61
|
+
elif args.command == "info":
|
|
62
|
+
show_info(args.snap_id)
|
|
63
|
+
|
|
64
|
+
elif args.command == "stats":
|
|
65
|
+
stats()
|
|
66
|
+
|
|
67
|
+
elif args.command == "list":
|
|
68
|
+
list_snap()
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
print_help()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Commands sub-package
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, os
|
|
2
|
+
from snapshot.utils.metadata_loader import load_metadata, save_metadata
|
|
3
|
+
from snapshot.utils.console import (
|
|
4
|
+
console, success, error, warning, info,
|
|
5
|
+
PRIMARY, WARNING, ERROR, MUTED,
|
|
6
|
+
)
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.prompt import Confirm
|
|
9
|
+
|
|
10
|
+
def delete(snap_id):
|
|
11
|
+
if not os.path.exists(snap_dir):
|
|
12
|
+
error("Project not initialized.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
metadata = load_metadata()
|
|
16
|
+
|
|
17
|
+
snapshot = next(
|
|
18
|
+
(
|
|
19
|
+
item
|
|
20
|
+
for item in metadata["snapshots"]
|
|
21
|
+
if item["save_id"] == snap_id
|
|
22
|
+
),
|
|
23
|
+
None,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if snapshot is None:
|
|
27
|
+
error(f"Snapshot #{snap_id} not found.")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
console.print(
|
|
31
|
+
Panel(
|
|
32
|
+
"[bold]This will permanently delete the selected snapshot.[/bold]\n"
|
|
33
|
+
"The snapshot archive and its metadata entry will be removed.",
|
|
34
|
+
title="[bold]! Snapshot Deletion[/bold]",
|
|
35
|
+
title_align="left",
|
|
36
|
+
border_style=WARNING,
|
|
37
|
+
padding=(1, 2),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
console.print()
|
|
41
|
+
|
|
42
|
+
confirmed = Confirm.ask(
|
|
43
|
+
f"Delete snapshot #{snap_id}?",
|
|
44
|
+
default=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if not confirmed:
|
|
48
|
+
info("Deletion cancelled.")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
zip_path = snapshot["file"]
|
|
52
|
+
|
|
53
|
+
if os.path.exists(zip_path):
|
|
54
|
+
os.remove(zip_path)
|
|
55
|
+
|
|
56
|
+
metadata["snapshots"] = [
|
|
57
|
+
item
|
|
58
|
+
for item in metadata["snapshots"]
|
|
59
|
+
if item["save_id"] != snap_id
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
save_metadata(metadata)
|
|
63
|
+
|
|
64
|
+
success(f"Deleted snapshot #{snap_id}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, os
|
|
2
|
+
from snapshot.utils.metadata_loader import load_metadata
|
|
3
|
+
from snapshot.utils.console import (
|
|
4
|
+
console,
|
|
5
|
+
error,
|
|
6
|
+
info,
|
|
7
|
+
PRIMARY, INFO, MUTED,
|
|
8
|
+
)
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def show_info(snap_id):
|
|
13
|
+
if not os.path.exists(snap_dir):
|
|
14
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
metadata = load_metadata()
|
|
18
|
+
|
|
19
|
+
snapshot = next(
|
|
20
|
+
(
|
|
21
|
+
item
|
|
22
|
+
for item in metadata["snapshots"]
|
|
23
|
+
if item["save_id"] == snap_id
|
|
24
|
+
),
|
|
25
|
+
None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if snapshot is None:
|
|
29
|
+
error(f"Snapshot [bold]#{snap_id}[/bold] not found.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
console.print()
|
|
33
|
+
console.print(
|
|
34
|
+
Panel(
|
|
35
|
+
f"[bold]Message:[/bold] [{INFO}]{snapshot['snap_message']}[/{INFO}]\n"
|
|
36
|
+
f"[bold]Created:[/bold] [{INFO}]{snapshot['created_on'][:19]}[/{INFO}]\n"
|
|
37
|
+
f"[bold]Archive:[/bold] [{MUTED}]{snapshot['file']}[/{MUTED}]",
|
|
38
|
+
title=f"[bold {PRIMARY}]Snapshot #{snapshot['save_id']}[/bold {PRIMARY}]",
|
|
39
|
+
border_style=MUTED,
|
|
40
|
+
padding=(1, 2),
|
|
41
|
+
expand=False,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
console.print()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from snapshot.config import os, snap_dir, snapshots_dir, metadata_default
|
|
2
|
+
from snapshot.utils.console import success, warning
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
def init():
|
|
6
|
+
|
|
7
|
+
# Snap directory creation
|
|
8
|
+
if os.path.exists(snap_dir):
|
|
9
|
+
warning("Project already initialized.")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
os.makedirs(snapshots_dir)
|
|
13
|
+
|
|
14
|
+
with open(f"{snap_dir}/metadata.json", "w") as file:
|
|
15
|
+
json.dump(metadata_default, file, indent=4)
|
|
16
|
+
|
|
17
|
+
success("Project initialized successfully!")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, os
|
|
2
|
+
from snapshot.utils.metadata_loader import load_metadata
|
|
3
|
+
from snapshot.utils.console import (
|
|
4
|
+
console, error, info, print_header,
|
|
5
|
+
PRIMARY, SUCCESS, MUTED, INFO,
|
|
6
|
+
)
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
def list_snap():
|
|
11
|
+
if not os.path.exists(snap_dir):
|
|
12
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
metadata = load_metadata()
|
|
16
|
+
snapshots = metadata["snapshots"]
|
|
17
|
+
|
|
18
|
+
print_header()
|
|
19
|
+
|
|
20
|
+
if not snapshots:
|
|
21
|
+
info("No snapshots found. Run [bold]snap save <message>[/bold] to create one.")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
table = Table(
|
|
25
|
+
show_header=True,
|
|
26
|
+
header_style=f"bold {PRIMARY}",
|
|
27
|
+
border_style=MUTED,
|
|
28
|
+
padding=(0, 2),
|
|
29
|
+
show_edge=False,
|
|
30
|
+
show_lines=True,
|
|
31
|
+
row_styles=["", "dim"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
table.add_column("#", style=f"bold {PRIMARY}", justify="right", min_width=4)
|
|
35
|
+
table.add_column("Message", style="bold white", min_width=20)
|
|
36
|
+
table.add_column("Created", style=INFO, min_width=20)
|
|
37
|
+
table.add_column("Archive Path", style=MUTED, min_width=16)
|
|
38
|
+
|
|
39
|
+
for snap in snapshots:
|
|
40
|
+
# Truncate the datetime for cleaner display
|
|
41
|
+
created = snap["created_on"][:19]
|
|
42
|
+
table.add_row(
|
|
43
|
+
str(snap["save_id"]),
|
|
44
|
+
snap["snap_message"],
|
|
45
|
+
created,
|
|
46
|
+
snap.get("file", "—"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
console.print(
|
|
50
|
+
Panel(
|
|
51
|
+
table,
|
|
52
|
+
title=f"[bold {PRIMARY}]Snapshots[/bold {PRIMARY}]",
|
|
53
|
+
subtitle=f"[dim]{len(snapshots)} snapshot{'s' if len(snapshots) != 1 else ''}[/dim]",
|
|
54
|
+
border_style=MUTED,
|
|
55
|
+
padding=(1, 1),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
console.print()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, os
|
|
2
|
+
from snapshot.utils.metadata_loader import load_metadata
|
|
3
|
+
from snapshot.commands.restore_snap import restore
|
|
4
|
+
|
|
5
|
+
from snapshot.utils.console import error
|
|
6
|
+
|
|
7
|
+
def restore_latest(is_clean=False):
|
|
8
|
+
if not os.path.exists(snap_dir):
|
|
9
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
metadata = load_metadata()
|
|
13
|
+
snapshots = metadata["snapshots"]
|
|
14
|
+
|
|
15
|
+
if not snapshots:
|
|
16
|
+
error("No snapshots found.")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
latest_snap = snapshots[-1]
|
|
20
|
+
|
|
21
|
+
restore(snap_id=latest_snap["save_id"], clean=is_clean)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, current_dir, os
|
|
2
|
+
from snapshot.utils.file_manager import unzip_dir, clear_contents
|
|
3
|
+
from snapshot.utils.metadata_loader import load_metadata
|
|
4
|
+
from snapshot.utils.console import (
|
|
5
|
+
console, success, error, warning, info,
|
|
6
|
+
PRIMARY, WARNING, ERROR, MUTED,
|
|
7
|
+
)
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm
|
|
10
|
+
|
|
11
|
+
def restore(snap_id, clean=False):
|
|
12
|
+
if not os.path.exists(snap_dir):
|
|
13
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
metadata = load_metadata()
|
|
17
|
+
|
|
18
|
+
snapshot = next(
|
|
19
|
+
(item for item in metadata["snapshots"] if item["save_id"] == snap_id),
|
|
20
|
+
None,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if snapshot is None:
|
|
24
|
+
error(f"Snapshot [bold]#{snap_id}[/bold] not found.")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
console.print()
|
|
29
|
+
info(f"Snapshot [bold]#{snapshot['save_id']}[/bold] — \"{snapshot['snap_message']}\"")
|
|
30
|
+
info(f"Created: {snapshot['created_on'][:19]}")
|
|
31
|
+
console.print()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if clean:
|
|
35
|
+
console.print(
|
|
36
|
+
Panel(
|
|
37
|
+
"[bold]This is a clean restore.[/bold]\n"
|
|
38
|
+
"All current project files will be [bold]permanently deleted[/bold]\n"
|
|
39
|
+
"before extracting the snapshot.",
|
|
40
|
+
title="[bold]! Destructive Operation[/bold]",
|
|
41
|
+
title_align="left",
|
|
42
|
+
border_style=WARNING,
|
|
43
|
+
padding=(1, 2),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
warning(
|
|
48
|
+
"Restoring will overwrite any conflicting files in the project directory."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
console.print()
|
|
52
|
+
|
|
53
|
+
confirmed = Confirm.ask(
|
|
54
|
+
f" [{PRIMARY}]Restore snapshot #{snap_id}?[/{PRIMARY}]",
|
|
55
|
+
default=False,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not confirmed:
|
|
59
|
+
info("Restore cancelled.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
with console.status(
|
|
63
|
+
f"[{PRIMARY}] Restoring snapshot #{snap_id}…[/{PRIMARY}]",
|
|
64
|
+
spinner="dots",
|
|
65
|
+
spinner_style=PRIMARY,
|
|
66
|
+
):
|
|
67
|
+
if clean:
|
|
68
|
+
clear_contents(current_dir)
|
|
69
|
+
|
|
70
|
+
zip_path = snapshot["file"]
|
|
71
|
+
unzip_dir(zip_path)
|
|
72
|
+
|
|
73
|
+
success(f"Snapshot [bold]#{snap_id}[/bold] restored successfully!")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, current_dir, os, snapshots_dir
|
|
2
|
+
from snapshot.utils.metadata_loader import save_metadata, load_metadata
|
|
3
|
+
from snapshot.utils.file_manager import zip_dir
|
|
4
|
+
from snapshot.utils.console import console, success, error, info, PRIMARY, MUTED
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def snapshot(snap_message):
|
|
8
|
+
if not os.path.exists(snap_dir):
|
|
9
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
metadata = load_metadata()
|
|
13
|
+
|
|
14
|
+
save_no = len(metadata["snapshots"]) + 1
|
|
15
|
+
zip_path = f"{snapshots_dir}/{save_no}.zip"
|
|
16
|
+
|
|
17
|
+
with console.status(
|
|
18
|
+
f"[{PRIMARY}] Creating snapshot #{save_no}…[/{PRIMARY}]",
|
|
19
|
+
spinner="dots",
|
|
20
|
+
spinner_style=PRIMARY,
|
|
21
|
+
):
|
|
22
|
+
zip_dir(current_dir, zip_path)
|
|
23
|
+
|
|
24
|
+
created_on = str(datetime.now())
|
|
25
|
+
|
|
26
|
+
metadata["snapshots"].append({
|
|
27
|
+
"snap_message": snap_message,
|
|
28
|
+
"save_id": save_no,
|
|
29
|
+
"file": zip_path,
|
|
30
|
+
"created_on": created_on
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
save_metadata(metadata)
|
|
34
|
+
|
|
35
|
+
success(f"Snapshot [bold]#{save_no}[/bold] saved — \"{snap_message}\"")
|
|
36
|
+
info(f"Archive: [{MUTED}]{zip_path}[/{MUTED}]")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from snapshot.config import snap_dir, os
|
|
2
|
+
from snapshot.utils.metadata_loader import load_metadata
|
|
3
|
+
from snapshot.utils.file_manager import get_directory_size, human_size
|
|
4
|
+
|
|
5
|
+
from snapshot.utils.console import (
|
|
6
|
+
console,
|
|
7
|
+
error,
|
|
8
|
+
PRIMARY,
|
|
9
|
+
MUTED,
|
|
10
|
+
)
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
def stats():
|
|
15
|
+
|
|
16
|
+
if not os.path.exists(snap_dir):
|
|
17
|
+
error("Project not initialized. Run [bold]snap init[/bold] first.")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
metadata = load_metadata()
|
|
21
|
+
snapshots = metadata["snapshots"]
|
|
22
|
+
|
|
23
|
+
snap_count = len(snapshots)
|
|
24
|
+
snap_size = get_directory_size(snap_dir)
|
|
25
|
+
|
|
26
|
+
latest_snap = snapshots[-1] if snapshots else None
|
|
27
|
+
oldest_snap = snapshots[0] if snapshots else None
|
|
28
|
+
|
|
29
|
+
table = Table(
|
|
30
|
+
show_header=True,
|
|
31
|
+
header_style=f"bold {PRIMARY}",
|
|
32
|
+
border_style=MUTED,
|
|
33
|
+
padding=(0, 2),
|
|
34
|
+
show_edge=False,
|
|
35
|
+
show_lines=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
table.add_column("Metric", style=f"bold {PRIMARY}")
|
|
39
|
+
table.add_column("Value", style="bold white")
|
|
40
|
+
|
|
41
|
+
table.add_row(
|
|
42
|
+
"Total Snapshots",
|
|
43
|
+
str(snap_count)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
table.add_row(
|
|
47
|
+
"Storage Used",
|
|
48
|
+
human_size(snap_size)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if oldest_snap:
|
|
52
|
+
table.add_row(
|
|
53
|
+
"Oldest Snapshot",
|
|
54
|
+
f"#{oldest_snap['save_id']}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if latest_snap:
|
|
58
|
+
table.add_row(
|
|
59
|
+
"Latest Snapshot",
|
|
60
|
+
f"#{latest_snap['save_id']}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
table.add_row(
|
|
64
|
+
"Latest Message",
|
|
65
|
+
latest_snap["snap_message"]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
table.add_row(
|
|
69
|
+
"Created",
|
|
70
|
+
latest_snap["created_on"][:19]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
console.print(
|
|
74
|
+
Panel(
|
|
75
|
+
table,
|
|
76
|
+
title=f"[bold {PRIMARY}]Statistics[/bold {PRIMARY}]",
|
|
77
|
+
subtitle=f"[dim]{snap_count} snapshot{'s' if snap_count != 1 else ''}[/dim]",
|
|
78
|
+
border_style=MUTED,
|
|
79
|
+
padding=(1, 1),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
console.print()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Utils sub-package
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.theme import Theme
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
import sys, io
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PRIMARY = "#3B82F6"
|
|
16
|
+
SUCCESS = "#10B981"
|
|
17
|
+
WARNING = "#F59E0B"
|
|
18
|
+
ERROR = "#EF4444"
|
|
19
|
+
INFO = "#06B6D4"
|
|
20
|
+
MUTED = "#6B7280"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
snap_theme = Theme({
|
|
24
|
+
"primary": PRIMARY,
|
|
25
|
+
"success": f"bold {SUCCESS}",
|
|
26
|
+
"warning": f"bold {WARNING}",
|
|
27
|
+
"error": f"bold {ERROR}",
|
|
28
|
+
"info": INFO,
|
|
29
|
+
"muted": MUTED,
|
|
30
|
+
"header": f"bold {PRIMARY}",
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
console = Console(theme=snap_theme)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def success(message: str) -> None:
|
|
38
|
+
|
|
39
|
+
console.print(f" [success]+[/success] {message}")
|
|
40
|
+
|
|
41
|
+
def error(message: str) -> None:
|
|
42
|
+
|
|
43
|
+
console.print(f" [error]x[/error] [error]{message}[/error]")
|
|
44
|
+
|
|
45
|
+
def warning(message: str) -> None:
|
|
46
|
+
|
|
47
|
+
console.print(f" [warning]![/warning] [warning]{message}[/warning]")
|
|
48
|
+
|
|
49
|
+
def info(message: str) -> None:
|
|
50
|
+
|
|
51
|
+
console.print(f" [info]>[/info] [info]{message}[/info]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
SNAP_LOGO = Text.from_markup(
|
|
56
|
+
f"[bold {PRIMARY}]Snap[/bold {PRIMARY}] [dim]- Local Project Snapshots[/dim]"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def print_header() -> None:
|
|
60
|
+
|
|
61
|
+
console.print()
|
|
62
|
+
console.print(
|
|
63
|
+
Panel(
|
|
64
|
+
SNAP_LOGO,
|
|
65
|
+
border_style=PRIMARY,
|
|
66
|
+
padding=(0, 2),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
console.print()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def print_help() -> None:
|
|
74
|
+
|
|
75
|
+
print_header()
|
|
76
|
+
|
|
77
|
+
table = Table(
|
|
78
|
+
show_header=True,
|
|
79
|
+
header_style=f"bold {PRIMARY}",
|
|
80
|
+
border_style=MUTED,
|
|
81
|
+
padding=(0, 2),
|
|
82
|
+
show_edge=False,
|
|
83
|
+
show_lines=False,
|
|
84
|
+
)
|
|
85
|
+
table.add_column("Command", style="bold white", min_width=28)
|
|
86
|
+
table.add_column("Description", style="dim")
|
|
87
|
+
|
|
88
|
+
table.add_row("snap init", "Initialize a new Snap project")
|
|
89
|
+
table.add_row("snap save <message>", "Save a snapshot with a message")
|
|
90
|
+
table.add_row("snap list", "List all saved snapshots")
|
|
91
|
+
table.add_row("snap restore <id>", "Restore a snapshot by ID")
|
|
92
|
+
table.add_row("snap restore <id> --clean", "Restore & remove current files first")
|
|
93
|
+
|
|
94
|
+
console.print(
|
|
95
|
+
Panel(
|
|
96
|
+
table,
|
|
97
|
+
title=f"[bold {PRIMARY}]Available Commands[/bold {PRIMARY}]",
|
|
98
|
+
border_style=MUTED,
|
|
99
|
+
padding=(1, 1),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
console.print()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from snapshot.config import current_dir, snap_dir, os
|
|
2
|
+
from snapshot.utils.snapignore import load_ignore_patterns, should_ignore
|
|
3
|
+
import zipfile, shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SYSTEM_PRESERVE = {
|
|
7
|
+
".snap",
|
|
8
|
+
".git"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def zip_dir(dir_path, zip_name):
|
|
12
|
+
|
|
13
|
+
ignore_patterns = load_ignore_patterns()
|
|
14
|
+
|
|
15
|
+
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
16
|
+
for root, dirs, files in os.walk(dir_path):
|
|
17
|
+
dirs[:] = [d for d in dirs if d != ".snap"]
|
|
18
|
+
dirs[:] = [d for d in dirs if d != ".git"]
|
|
19
|
+
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d), ignore_patterns)]
|
|
20
|
+
for file in files:
|
|
21
|
+
file_path = os.path.join(root, file)
|
|
22
|
+
if should_ignore(file_path, ignore_patterns):
|
|
23
|
+
continue
|
|
24
|
+
# arcname preserves directory structure relative to dir_path
|
|
25
|
+
arcname = os.path.relpath(file_path, start=dir_path)
|
|
26
|
+
zipf.write(file_path, arcname)
|
|
27
|
+
|
|
28
|
+
def unzip_dir(zip_path):
|
|
29
|
+
# Extract all files to a directory
|
|
30
|
+
with zipfile.ZipFile(f'{zip_path}', 'r') as zip_ref:
|
|
31
|
+
zip_ref.extractall(current_dir)
|
|
32
|
+
|
|
33
|
+
def clear_contents(dir_path):
|
|
34
|
+
folder = Path(dir_path)
|
|
35
|
+
|
|
36
|
+
for item in folder.iterdir():
|
|
37
|
+
|
|
38
|
+
if item.name in SYSTEM_PRESERVE:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if item.is_file() or item.is_symlink():
|
|
42
|
+
item.unlink()
|
|
43
|
+
|
|
44
|
+
elif item.is_dir():
|
|
45
|
+
shutil.rmtree(item)
|
|
46
|
+
|
|
47
|
+
def get_directory_size(path):
|
|
48
|
+
total_size = 0
|
|
49
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
50
|
+
for f in filenames:
|
|
51
|
+
fp = os.path.join(dirpath, f)
|
|
52
|
+
# Skip symbolic links to avoid infinite loops or errors
|
|
53
|
+
if not os.path.islink(fp):
|
|
54
|
+
total_size += os.path.getsize(fp)
|
|
55
|
+
return total_size
|
|
56
|
+
|
|
57
|
+
def human_size(size):
|
|
58
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
59
|
+
if size < 1024:
|
|
60
|
+
return f"{size:.1f} {unit}"
|
|
61
|
+
size /= 1024
|
|
62
|
+
return f"{size:.1f} PB"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from snapshot.config import snap_dir
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def load_metadata():
|
|
6
|
+
with open(f"{snap_dir}/metadata.json", "r") as file:
|
|
7
|
+
return json.load(file)
|
|
8
|
+
|
|
9
|
+
def save_metadata(data):
|
|
10
|
+
with open(f"{snap_dir}/metadata.json", "w") as file:
|
|
11
|
+
json.dump(data, file, indent=4)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from snapshot.config import current_dir, os
|
|
2
|
+
from fnmatch import fnmatch
|
|
3
|
+
|
|
4
|
+
def load_ignore_patterns():
|
|
5
|
+
snap_ignore_file = f"{current_dir}/.snapignore"
|
|
6
|
+
|
|
7
|
+
if not os.path.exists(snap_ignore_file):
|
|
8
|
+
return []
|
|
9
|
+
|
|
10
|
+
with open(snap_ignore_file, "r") as file:
|
|
11
|
+
patterns = file.readlines()
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
line.strip()
|
|
15
|
+
for line in patterns
|
|
16
|
+
if line.strip() and not line.strip().startswith("#")
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def should_ignore(file_path, ignore_patterns):
|
|
20
|
+
relative_path = os.path.relpath(file_path, current_dir)
|
|
21
|
+
relative_path = relative_path.replace("\\", "/")
|
|
22
|
+
|
|
23
|
+
for pattern in ignore_patterns:
|
|
24
|
+
pattern = pattern.strip()
|
|
25
|
+
|
|
26
|
+
# Folder ignore
|
|
27
|
+
if pattern.endswith("/"):
|
|
28
|
+
folder = pattern.rstrip("/")
|
|
29
|
+
if relative_path.startswith(folder + "/"):
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
# Wildcard/file ignore
|
|
33
|
+
if fnmatch(relative_path, pattern):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
return False
|