gitstore 0.1.0__tar.gz → 0.1.2__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.
- {gitstore-0.1.0 → gitstore-0.1.2}/PKG-INFO +2 -2
- {gitstore-0.1.0 → gitstore-0.1.2}/README.md +4 -4
- {gitstore-0.1.0 → gitstore-0.1.2}/pyproject.toml +1 -1
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore/client.py +165 -51
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore.egg-info/PKG-INFO +2 -2
- {gitstore-0.1.0 → gitstore-0.1.2}/setup.cfg +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore/__init__.py +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore/config.py +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore/crypto_ops.py +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore/github_ops.py +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore.egg-info/SOURCES.txt +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore.egg-info/dependency_links.txt +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore.egg-info/requires.txt +0 -0
- {gitstore-0.1.0 → gitstore-0.1.2}/src/gitstore.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitstore
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories.
|
|
5
5
|
Author: artitzco
|
|
6
6
|
License: MIT
|
|
@@ -55,7 +55,7 @@ from gitstore import GitStoreUploader, GitStoreDownloader
|
|
|
55
55
|
from gitstore import GitStoreUploader
|
|
56
56
|
|
|
57
57
|
uploader = GitStoreUploader(
|
|
58
|
-
repo_path="C:/repos/my-publish-repo", #
|
|
58
|
+
repo_path="C:/repos/my-publish-repo", # required
|
|
59
59
|
security_level="high", # default
|
|
60
60
|
)
|
|
61
61
|
|
|
@@ -43,10 +43,10 @@ from gitstore import GitStoreUploader, GitStoreDownloader
|
|
|
43
43
|
```python
|
|
44
44
|
from gitstore import GitStoreUploader
|
|
45
45
|
|
|
46
|
-
uploader = GitStoreUploader(
|
|
47
|
-
repo_path="C:/repos/my-publish-repo", #
|
|
48
|
-
security_level="high", # default
|
|
49
|
-
)
|
|
46
|
+
uploader = GitStoreUploader(
|
|
47
|
+
repo_path="C:/repos/my-publish-repo", # required
|
|
48
|
+
security_level="high", # default
|
|
49
|
+
)
|
|
50
50
|
|
|
51
51
|
record = uploader.store(
|
|
52
52
|
source_path="C:/data/documento.pdf", # file or directory
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitstore"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -2,6 +2,8 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import shutil
|
|
4
4
|
import tempfile
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
from pathlib import Path
|
|
@@ -17,6 +19,7 @@ DEFAULT_PASSWORD_ENV_VAR = "GITSTORE_PASSWORD"
|
|
|
17
19
|
class StoredArtifact:
|
|
18
20
|
name: str
|
|
19
21
|
artifact: str
|
|
22
|
+
artifact_hash: str
|
|
20
23
|
is_directory: bool
|
|
21
24
|
created_at_utc: str
|
|
22
25
|
|
|
@@ -31,19 +34,34 @@ def _resolve_password(password: str | None, password_env_var: str) -> str:
|
|
|
31
34
|
return resolved_password
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
def _hash_from_name_or_content(encrypted_path: Path) -> str:
|
|
38
|
+
# utilitz default output name pattern: -confidential-<24hex>.asc
|
|
39
|
+
m = re.search(r"-confidential-([0-9a-fA-F]{24})", encrypted_path.name)
|
|
40
|
+
if m:
|
|
41
|
+
return m.group(1).lower()
|
|
42
|
+
|
|
43
|
+
hasher = hashlib.sha256()
|
|
44
|
+
with open(encrypted_path, "rb") as f:
|
|
45
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
46
|
+
hasher.update(chunk)
|
|
47
|
+
return hasher.hexdigest()[:24]
|
|
48
|
+
|
|
49
|
+
|
|
34
50
|
class GitStoreUploader:
|
|
35
51
|
def __init__(
|
|
36
52
|
self,
|
|
37
|
-
repo_path: str
|
|
53
|
+
repo_path: str,
|
|
38
54
|
password: str | None = None,
|
|
39
55
|
vault_dir: str = "vault",
|
|
40
56
|
request_timeout: int = 60,
|
|
41
57
|
password_env_var: str = DEFAULT_PASSWORD_ENV_VAR,
|
|
42
58
|
security_level: str = "high",
|
|
43
59
|
) -> None:
|
|
60
|
+
if not repo_path:
|
|
61
|
+
raise ValueError("repo_path is required for uploader.")
|
|
44
62
|
resolved_password = _resolve_password(password, password_env_var)
|
|
45
63
|
self.config = GitStoreConfig(password=resolved_password, request_timeout=request_timeout)
|
|
46
|
-
self.repo_path = Path(repo_path).expanduser().resolve()
|
|
64
|
+
self.repo_path = Path(repo_path).expanduser().resolve()
|
|
47
65
|
self.vault_dir = vault_dir.strip().strip("/\\") or "vault"
|
|
48
66
|
self._manifest_rel = f"{self.vault_dir}/index.json"
|
|
49
67
|
self.password_env_var = password_env_var
|
|
@@ -82,52 +100,70 @@ class GitStoreUploader:
|
|
|
82
100
|
security_level=level,
|
|
83
101
|
)
|
|
84
102
|
)
|
|
85
|
-
extension = "".join(encrypted_path.suffixes) or ".asc"
|
|
86
|
-
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
87
|
-
artifact_filename = f"{name}__{stamp}{extension}"
|
|
88
|
-
artifact_rel = f"{self.vault_dir}/{artifact_filename}"
|
|
89
|
-
artifact_abs = self.repo_path / artifact_rel
|
|
90
|
-
artifact_abs.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
shutil.copy2(encrypted_path, artifact_abs)
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
old_artifact = manifest[name].get("artifact")
|
|
101
|
-
if old_artifact:
|
|
102
|
-
old_artifact_rel = f"{self.vault_dir}/{old_artifact}"
|
|
103
|
-
old_artifact_abs = self.repo_path / old_artifact_rel
|
|
104
|
-
if old_artifact_abs.exists():
|
|
105
|
-
old_artifact_abs.unlink()
|
|
106
|
-
|
|
107
|
-
record = StoredArtifact(
|
|
108
|
-
name=name,
|
|
109
|
-
artifact=artifact_filename,
|
|
110
|
-
is_directory=is_directory,
|
|
111
|
-
created_at_utc=datetime.now(timezone.utc).isoformat(),
|
|
112
|
-
)
|
|
113
|
-
manifest[name] = record.__dict__
|
|
114
|
-
self._save_manifest_local(manifest)
|
|
104
|
+
try:
|
|
105
|
+
extension = "".join(encrypted_path.suffixes) or ".asc"
|
|
106
|
+
artifact_hash = _hash_from_name_or_content(encrypted_path)
|
|
107
|
+
artifact_filename = f"{name}{extension}"
|
|
108
|
+
artifact_rel = f"{self.vault_dir}/{artifact_filename}"
|
|
109
|
+
artifact_abs = self.repo_path / artifact_rel
|
|
110
|
+
artifact_abs.parent.mkdir(parents=True, exist_ok=True)
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
manifest = self._load_manifest_local()
|
|
113
|
+
existing_record = manifest.get(name)
|
|
114
|
+
if existing_record:
|
|
115
|
+
existing_hash = str(existing_record.get("artifact_hash", "")).lower()
|
|
116
|
+
if existing_hash == artifact_hash:
|
|
117
|
+
# Same content already stored for the same logical name.
|
|
118
|
+
print(
|
|
119
|
+
f"[gitstore] Skip upload: '{name}' already up to date "
|
|
120
|
+
f"(hash={artifact_hash})."
|
|
121
|
+
)
|
|
122
|
+
return StoredArtifact(
|
|
123
|
+
name=name,
|
|
124
|
+
artifact=str(existing_record["artifact"]),
|
|
125
|
+
artifact_hash=existing_hash,
|
|
126
|
+
is_directory=bool(existing_record["is_directory"]),
|
|
127
|
+
created_at_utc=str(existing_record["created_at_utc"]),
|
|
128
|
+
)
|
|
129
|
+
if not replace_existing:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Name '{name}' already exists. Use replace_existing=True to replace it."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
shutil.copy2(encrypted_path, artifact_abs)
|
|
135
|
+
|
|
136
|
+
record = StoredArtifact(
|
|
137
|
+
name=name,
|
|
138
|
+
artifact=artifact_filename,
|
|
139
|
+
artifact_hash=artifact_hash,
|
|
140
|
+
is_directory=is_directory,
|
|
141
|
+
created_at_utc=datetime.now(timezone.utc).isoformat(),
|
|
142
|
+
)
|
|
143
|
+
manifest[name] = {
|
|
144
|
+
"name": record.name,
|
|
145
|
+
"artifact": record.artifact,
|
|
146
|
+
"artifact_hash": record.artifact_hash,
|
|
147
|
+
"is_directory": record.is_directory,
|
|
148
|
+
"created_at_utc": record.created_at_utc,
|
|
149
|
+
}
|
|
150
|
+
self._save_manifest_local(manifest)
|
|
151
|
+
|
|
152
|
+
message = commit_message or f"gitstore: store '{name}'"
|
|
153
|
+
paths_to_commit = [artifact_rel, self._manifest_rel]
|
|
154
|
+
git_add_commit_push(
|
|
127
155
|
repo_path=str(self.repo_path),
|
|
128
|
-
|
|
156
|
+
paths_in_repo=paths_to_commit,
|
|
157
|
+
commit_message=message,
|
|
129
158
|
)
|
|
130
|
-
|
|
159
|
+
print(
|
|
160
|
+
f"[gitstore] Uploaded '{name}' -> '{artifact_rel}' "
|
|
161
|
+
f"(hash={artifact_hash})."
|
|
162
|
+
)
|
|
163
|
+
return record
|
|
164
|
+
finally:
|
|
165
|
+
if encrypted_path.exists():
|
|
166
|
+
encrypted_path.unlink()
|
|
131
167
|
|
|
132
168
|
def destroy(self, name: str, commit_message: str | None = None) -> None:
|
|
133
169
|
self._ensure_repo()
|
|
@@ -154,6 +190,7 @@ class GitStoreUploader:
|
|
|
154
190
|
commit_message=message,
|
|
155
191
|
)
|
|
156
192
|
git_purge_path_from_history(repo_path=str(self.repo_path), path_in_repo=artifact_rel)
|
|
193
|
+
print(f"[gitstore] Destroyed '{name}' and purged '{artifact_rel}' from history.")
|
|
157
194
|
|
|
158
195
|
def _ensure_repo(self) -> None:
|
|
159
196
|
if not (self.repo_path / ".git").exists():
|
|
@@ -176,7 +213,7 @@ class GitStoreUploader:
|
|
|
176
213
|
manifest_path = self._manifest_path()
|
|
177
214
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
178
215
|
with open(manifest_path, "w", encoding="utf-8", newline="\n") as f:
|
|
179
|
-
json.dump(manifest, f, indent=2, sort_keys=
|
|
216
|
+
json.dump(manifest, f, indent=2, sort_keys=False)
|
|
180
217
|
f.write("\n")
|
|
181
218
|
|
|
182
219
|
|
|
@@ -206,7 +243,34 @@ class GitStoreDownloader:
|
|
|
206
243
|
) -> str:
|
|
207
244
|
record = self._get_manifest_record(name)
|
|
208
245
|
artifact = record["artifact"]
|
|
246
|
+
artifact_hash = str(record.get("artifact_hash") or "")
|
|
209
247
|
is_directory = bool(record["is_directory"])
|
|
248
|
+
local_index_path, expected_existing = self._local_registry_paths(
|
|
249
|
+
output_path=output_path,
|
|
250
|
+
is_directory=is_directory,
|
|
251
|
+
name=name,
|
|
252
|
+
)
|
|
253
|
+
local_index = self._load_local_download_index(local_index_path)
|
|
254
|
+
already_restored = False
|
|
255
|
+
for item in local_index:
|
|
256
|
+
if not isinstance(item, dict):
|
|
257
|
+
continue
|
|
258
|
+
if str(item.get("name") or "") != name:
|
|
259
|
+
continue
|
|
260
|
+
if str(item.get("artifact_hash") or "") != artifact_hash:
|
|
261
|
+
continue
|
|
262
|
+
recorded_path = str(item.get("restored_path") or "")
|
|
263
|
+
recorded_exists = Path(recorded_path).exists() if recorded_path else False
|
|
264
|
+
expected_exists = expected_existing.exists() if expected_existing else False
|
|
265
|
+
if expected_exists or recorded_exists:
|
|
266
|
+
already_restored = True
|
|
267
|
+
break
|
|
268
|
+
if already_restored:
|
|
269
|
+
print(
|
|
270
|
+
f"[gitstore] Skip download: '{name}' already restored "
|
|
271
|
+
f"(hash={artifact_hash})."
|
|
272
|
+
)
|
|
273
|
+
return str(expected_existing)
|
|
210
274
|
|
|
211
275
|
temp_dir = Path(tempfile.mkdtemp(prefix="gitstore_restore_"))
|
|
212
276
|
temp_file = temp_dir / "artifact.enc"
|
|
@@ -214,18 +278,34 @@ class GitStoreDownloader:
|
|
|
214
278
|
raw_url = f"{self.raw_base_url}/{self.vault_dir}/{artifact}"
|
|
215
279
|
download_raw_file(raw_url, str(temp_file), timeout=self.config.request_timeout)
|
|
216
280
|
if is_directory:
|
|
217
|
-
|
|
281
|
+
restored_path = decrypt_directory(
|
|
282
|
+
encrypted_path=str(temp_file),
|
|
283
|
+
config=self.config,
|
|
284
|
+
output_path=output_path,
|
|
285
|
+
overwrite=overwrite,
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
restored_path = decrypt_file(
|
|
218
289
|
encrypted_path=str(temp_file),
|
|
219
290
|
config=self.config,
|
|
220
291
|
output_path=output_path,
|
|
221
292
|
overwrite=overwrite,
|
|
222
293
|
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
294
|
+
local_index.append({
|
|
295
|
+
"name": name,
|
|
296
|
+
"artifact": artifact,
|
|
297
|
+
"artifact_hash": artifact_hash,
|
|
298
|
+
"is_directory": is_directory,
|
|
299
|
+
"created_at_utc": str(record.get("created_at_utc", "")),
|
|
300
|
+
"restored_path": restored_path,
|
|
301
|
+
"downloaded_at_utc": datetime.now(timezone.utc).isoformat(),
|
|
302
|
+
})
|
|
303
|
+
self._save_local_download_index(local_index_path, local_index)
|
|
304
|
+
print(
|
|
305
|
+
f"[gitstore] Downloaded and restored '{name}' -> '{restored_path}' "
|
|
306
|
+
f"(hash={artifact_hash})."
|
|
228
307
|
)
|
|
308
|
+
return restored_path
|
|
229
309
|
finally:
|
|
230
310
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
231
311
|
|
|
@@ -247,3 +327,37 @@ class GitStoreDownloader:
|
|
|
247
327
|
raise ValueError(f"Invalid manifest record for name: {name}")
|
|
248
328
|
return record
|
|
249
329
|
|
|
330
|
+
def _local_registry_paths(
|
|
331
|
+
self,
|
|
332
|
+
output_path: str | None,
|
|
333
|
+
is_directory: bool,
|
|
334
|
+
name: str,
|
|
335
|
+
) -> tuple[Path, Path | None]:
|
|
336
|
+
if output_path:
|
|
337
|
+
out = Path(output_path).expanduser().resolve()
|
|
338
|
+
target = out
|
|
339
|
+
else:
|
|
340
|
+
# Conservative default when no explicit output path is passed.
|
|
341
|
+
base = Path.cwd().resolve()
|
|
342
|
+
target = base / name
|
|
343
|
+
|
|
344
|
+
# Keep metadata file at the same level as the restored artifact.
|
|
345
|
+
base_dir = target.parent
|
|
346
|
+
idx = base_dir / ".gitstore.json"
|
|
347
|
+
return idx, target
|
|
348
|
+
|
|
349
|
+
def _load_local_download_index(self, index_path: Path) -> list[dict]:
|
|
350
|
+
if not index_path.exists():
|
|
351
|
+
return []
|
|
352
|
+
with open(index_path, "r", encoding="utf-8") as f:
|
|
353
|
+
data = json.load(f)
|
|
354
|
+
if not isinstance(data, list):
|
|
355
|
+
raise ValueError(f"Invalid local download index format: {index_path}")
|
|
356
|
+
return [item for item in data if isinstance(item, dict)]
|
|
357
|
+
|
|
358
|
+
def _save_local_download_index(self, index_path: Path, data: list[dict]) -> None:
|
|
359
|
+
index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
with open(index_path, "w", encoding="utf-8", newline="\n") as f:
|
|
361
|
+
json.dump(data, f, indent=2, sort_keys=False)
|
|
362
|
+
f.write("\n")
|
|
363
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitstore
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories.
|
|
5
5
|
Author: artitzco
|
|
6
6
|
License: MIT
|
|
@@ -55,7 +55,7 @@ from gitstore import GitStoreUploader, GitStoreDownloader
|
|
|
55
55
|
from gitstore import GitStoreUploader
|
|
56
56
|
|
|
57
57
|
uploader = GitStoreUploader(
|
|
58
|
-
repo_path="C:/repos/my-publish-repo", #
|
|
58
|
+
repo_path="C:/repos/my-publish-repo", # required
|
|
59
59
|
security_level="high", # default
|
|
60
60
|
)
|
|
61
61
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|