flatback 1.0__tar.gz → 1.1__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.
- {flatback-1.0 → flatback-1.1}/PKG-INFO +3 -3
- {flatback-1.0 → flatback-1.1}/README.md +2 -0
- {flatback-1.0 → flatback-1.1}/flatback/core.py +65 -6
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/PKG-INFO +3 -3
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/SOURCES.txt +0 -1
- {flatback-1.0 → flatback-1.1}/pyproject.toml +1 -1
- flatback-1.0/setup.py +0 -22
- {flatback-1.0 → flatback-1.1}/flatback/__init__.py +0 -0
- {flatback-1.0 → flatback-1.1}/flatback/__main__.py +0 -0
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/dependency_links.txt +0 -0
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/entry_points.txt +0 -0
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/requires.txt +0 -0
- {flatback-1.0 → flatback-1.1}/flatback.egg-info/top_level.txt +0 -0
- {flatback-1.0 → flatback-1.1}/setup.cfg +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatback
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: Backup and restore Flatpak applications for offline transfer
|
|
5
|
-
Home-page: https://github.com/jfhack/flatback
|
|
6
5
|
Project-URL: Homepage, https://github.com/jfhack/flatback
|
|
7
6
|
Requires-Python: >=3.8
|
|
8
7
|
Description-Content-Type: text/markdown
|
|
9
8
|
Requires-Dist: pyyaml
|
|
10
|
-
Dynamic: home-page
|
|
11
9
|
|
|
12
10
|
# Flatback
|
|
13
11
|
|
|
@@ -52,6 +50,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
|
|
|
52
50
|
|
|
53
51
|
**Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
|
|
54
52
|
|
|
53
|
+
> **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
|
|
54
|
+
|
|
55
55
|
### Manifest Example (`flatback.yml`)
|
|
56
56
|
|
|
57
57
|
```yaml
|
|
@@ -41,6 +41,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
|
|
|
41
41
|
|
|
42
42
|
**Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
|
|
43
43
|
|
|
44
|
+
> **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
|
|
45
|
+
|
|
44
46
|
### Manifest Example (`flatback.yml`)
|
|
45
47
|
|
|
46
48
|
```yaml
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
import subprocess
|
|
2
3
|
import yaml
|
|
3
4
|
import shutil
|
|
@@ -8,6 +9,25 @@ from pathlib import Path
|
|
|
8
9
|
from . import __version__
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
_APP_ID_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_-][A-Za-z0-9_-]*){2,}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_valid_app_id(app_id):
|
|
16
|
+
return isinstance(app_id, str) and len(app_id) <= 255 and _APP_ID_RE.fullmatch(app_id) is not None
|
|
17
|
+
|
|
18
|
+
def _is_within(path, base):
|
|
19
|
+
try:
|
|
20
|
+
Path(path).relative_to(base)
|
|
21
|
+
return True
|
|
22
|
+
except ValueError:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ignore_special_files(src_dir, names):
|
|
27
|
+
paths = (Path(src_dir) / name for name in names)
|
|
28
|
+
return {p.name for p in paths if not (p.is_symlink() or p.is_dir() or p.is_file())}
|
|
29
|
+
|
|
30
|
+
|
|
11
31
|
class FlatpakRef:
|
|
12
32
|
def __init__(self, app_id, arch="x86_64", branch="stable", origin="flathub", installation="user"):
|
|
13
33
|
self.app_id = app_id
|
|
@@ -159,7 +179,7 @@ class Backup:
|
|
|
159
179
|
out = Path(output_path or default_name)
|
|
160
180
|
if out.exists():
|
|
161
181
|
shutil.rmtree(out)
|
|
162
|
-
shutil.copytree(tmp_dir, out)
|
|
182
|
+
shutil.copytree(tmp_dir, out, symlinks=True)
|
|
163
183
|
|
|
164
184
|
print(f"Backup saved to: {out}")
|
|
165
185
|
return out
|
|
@@ -171,7 +191,7 @@ class Backup:
|
|
|
171
191
|
return False
|
|
172
192
|
dst = tmp_dir / self.DATA_DIR
|
|
173
193
|
print(f"Copying user data: {src}")
|
|
174
|
-
shutil.copytree(src, dst, symlinks=True)
|
|
194
|
+
shutil.copytree(src, dst, symlinks=True, ignore=_ignore_special_files)
|
|
175
195
|
return True
|
|
176
196
|
|
|
177
197
|
def _pack_tar(self, source_dir, tar_path):
|
|
@@ -202,7 +222,7 @@ class Restore:
|
|
|
202
222
|
with tempfile.TemporaryDirectory() as tmp:
|
|
203
223
|
tmp_dir = Path(tmp)
|
|
204
224
|
with tarfile.open(tar_path, "r:gz") as tar:
|
|
205
|
-
tar.extractall(tmp_dir, filter=
|
|
225
|
+
tar.extractall(tmp_dir, filter=self._data_filter)
|
|
206
226
|
self._load_from_dir(tmp_dir)
|
|
207
227
|
|
|
208
228
|
def _load_from_dir(self, directory):
|
|
@@ -212,12 +232,16 @@ class Restore:
|
|
|
212
232
|
|
|
213
233
|
with open(manifest_path) as f:
|
|
214
234
|
manifest = yaml.safe_load(f)
|
|
235
|
+
if not isinstance(manifest, dict):
|
|
236
|
+
raise ValueError(f"Invalid manifest: {manifest_path}")
|
|
215
237
|
|
|
216
|
-
runtime_bundle = directory
|
|
217
|
-
app_bundle = directory
|
|
238
|
+
runtime_bundle = self._bundle_path(directory, manifest.get("runtime_bundle"), "runtime_bundle")
|
|
239
|
+
app_bundle = self._bundle_path(directory, manifest.get("app_bundle"), "app_bundle")
|
|
218
240
|
|
|
219
241
|
runtime_ref = FlatpakRef.from_dict(manifest["runtime"])
|
|
220
242
|
app_ref = FlatpakRef.from_dict(manifest["app"])
|
|
243
|
+
if not is_valid_app_id(app_ref.app_id):
|
|
244
|
+
raise ValueError(f"Invalid app_id in manifest: {app_ref.app_id!r}")
|
|
221
245
|
|
|
222
246
|
print(f"Installing runtime: {runtime_ref.ref}")
|
|
223
247
|
self._install_bundle(runtime_bundle)
|
|
@@ -230,10 +254,44 @@ class Restore:
|
|
|
230
254
|
|
|
231
255
|
print(f"Restore complete: {app_ref.app_id}")
|
|
232
256
|
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _data_filter(member, path):
|
|
259
|
+
try:
|
|
260
|
+
return tarfile.data_filter(member, path)
|
|
261
|
+
except tarfile.FilterError as e:
|
|
262
|
+
print(f"Skipping unsafe archive entry: {e}")
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _bundle_path(directory, name, field):
|
|
267
|
+
if not isinstance(name, str) or name in ("", ".", "..") or Path(name).name != name:
|
|
268
|
+
raise ValueError(f"Invalid {field} in manifest: {name!r}")
|
|
269
|
+
path = directory / name
|
|
270
|
+
if not _is_within(path.resolve(), directory.resolve()):
|
|
271
|
+
raise ValueError(f"{field} points outside the backup: {name!r}")
|
|
272
|
+
if not path.is_file():
|
|
273
|
+
raise FileNotFoundError(f"Bundle not found in backup: {path}")
|
|
274
|
+
return path
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _remove_escaping_symlinks(root):
|
|
278
|
+
root = root.resolve()
|
|
279
|
+
for link in [p for p in root.rglob("*") if p.is_symlink()]:
|
|
280
|
+
try:
|
|
281
|
+
target = link.resolve()
|
|
282
|
+
except (OSError, RuntimeError):
|
|
283
|
+
target = None
|
|
284
|
+
if target is None or not _is_within(target, root):
|
|
285
|
+
print(f"Removed symlink pointing outside the app data: {link.relative_to(root)} -> {target or 'symlink loop'}")
|
|
286
|
+
link.unlink()
|
|
287
|
+
|
|
233
288
|
def _handle_user_data(self, directory, app_id):
|
|
234
289
|
data_dir = directory / self.DATA_DIR
|
|
235
290
|
if not data_dir.exists():
|
|
236
291
|
return
|
|
292
|
+
if data_dir.is_symlink() or not data_dir.is_dir():
|
|
293
|
+
print(f"Skipping user data: {data_dir} is not a regular directory")
|
|
294
|
+
return
|
|
237
295
|
|
|
238
296
|
should_restore = self._restore_data
|
|
239
297
|
if should_restore is None:
|
|
@@ -249,7 +307,8 @@ class Restore:
|
|
|
249
307
|
print(f"Replacing existing user data: {dst}")
|
|
250
308
|
shutil.rmtree(dst)
|
|
251
309
|
print(f"Restoring user data to: {dst}")
|
|
252
|
-
shutil.copytree(data_dir, dst, symlinks=True)
|
|
310
|
+
shutil.copytree(data_dir, dst, symlinks=True, ignore=_ignore_special_files)
|
|
311
|
+
self._remove_escaping_symlinks(dst)
|
|
253
312
|
|
|
254
313
|
def _install_bundle(self, bundle_path):
|
|
255
314
|
result = subprocess.run(
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatback
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: Backup and restore Flatpak applications for offline transfer
|
|
5
|
-
Home-page: https://github.com/jfhack/flatback
|
|
6
5
|
Project-URL: Homepage, https://github.com/jfhack/flatback
|
|
7
6
|
Requires-Python: >=3.8
|
|
8
7
|
Description-Content-Type: text/markdown
|
|
9
8
|
Requires-Dist: pyyaml
|
|
10
|
-
Dynamic: home-page
|
|
11
9
|
|
|
12
10
|
# Flatback
|
|
13
11
|
|
|
@@ -52,6 +50,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
|
|
|
52
50
|
|
|
53
51
|
**Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
|
|
54
52
|
|
|
53
|
+
> **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
|
|
54
|
+
|
|
55
55
|
### Manifest Example (`flatback.yml`)
|
|
56
56
|
|
|
57
57
|
```yaml
|
flatback-1.0/setup.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
from setuptools import setup, find_packages
|
|
3
|
-
|
|
4
|
-
with open("README.md", "r", encoding="utf-8") as fh:
|
|
5
|
-
long_description = fh.read()
|
|
6
|
-
|
|
7
|
-
setup(
|
|
8
|
-
name="flatback",
|
|
9
|
-
version="1.0",
|
|
10
|
-
packages=find_packages(),
|
|
11
|
-
entry_points={
|
|
12
|
-
"console_scripts": [
|
|
13
|
-
"flatback = flatback.__main__:main",
|
|
14
|
-
],
|
|
15
|
-
},
|
|
16
|
-
url="https://github.com/jfhack/flatback",
|
|
17
|
-
install_requires=[
|
|
18
|
-
"pyyaml",
|
|
19
|
-
],
|
|
20
|
-
long_description=long_description,
|
|
21
|
-
long_description_content_type="text/markdown",
|
|
22
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|