flatback 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flatback/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0"
flatback/__main__.py ADDED
@@ -0,0 +1,48 @@
1
+ import argparse
2
+ import sys
3
+ from .core import Backup, Restore
4
+
5
+
6
+ def parse_args():
7
+ parser = argparse.ArgumentParser(
8
+ prog="flatback",
9
+ description="Backup and restore Flatpak applications for offline transfer",
10
+ )
11
+ sub = parser.add_subparsers(dest="command", required=True)
12
+
13
+ backup = sub.add_parser("backup", help="Backup a Flatpak application")
14
+ backup.add_argument("app_id", help="Flatpak application ID (e.g. org.yuzu_emu.yuzu)")
15
+ backup.add_argument("-o", "--output", help="Output path (default: <app_id>.tar.gz)")
16
+ backup.add_argument("--no-tar", action="store_true", help="Save as directory instead of tar.gz")
17
+ backup.add_argument("--data", action="store_true", help="Include user data (~/.var/app/<app_id>) in backup")
18
+ backup.add_argument("--system", action="store_true", help="Use system installation instead of user")
19
+
20
+ restore = sub.add_parser("restore", help="Restore a Flatpak application from backup")
21
+ restore.add_argument("source", help="Path to backup tar.gz or directory")
22
+ restore.add_argument("--data", action="store_true", default=None, help="Restore user data without prompting")
23
+ restore.add_argument("--no-data", action="store_true", help="Skip user data restore without prompting")
24
+ restore.add_argument("--system", action="store_true", help="Install to system instead of user")
25
+
26
+ return parser.parse_args()
27
+
28
+
29
+ def main():
30
+ args = parse_args()
31
+ installation = "system" if args.system else "user"
32
+
33
+ if args.command == "backup":
34
+ bk = Backup(args.app_id, installation)
35
+ bk.create(output_path=args.output, as_tar=not args.no_tar, include_data=args.data)
36
+
37
+ elif args.command == "restore":
38
+ restore_data = None
39
+ if args.data:
40
+ restore_data = True
41
+ elif args.no_data:
42
+ restore_data = False
43
+ rs = Restore(installation, restore_data=restore_data)
44
+ rs.load(args.source)
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
flatback/core.py ADDED
@@ -0,0 +1,264 @@
1
+ import subprocess
2
+ import yaml
3
+ import shutil
4
+ import tarfile
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+
10
+
11
+ class FlatpakRef:
12
+ def __init__(self, app_id, arch="x86_64", branch="stable", origin="flathub", installation="user"):
13
+ self.app_id = app_id
14
+ self.arch = arch
15
+ self.branch = branch
16
+ self.origin = origin
17
+ self.installation = installation
18
+
19
+ @property
20
+ def ref(self):
21
+ return f"{self.app_id}/{self.arch}/{self.branch}"
22
+
23
+ def as_dict(self):
24
+ return {
25
+ "app_id": self.app_id,
26
+ "arch": self.arch,
27
+ "branch": self.branch,
28
+ "origin": self.origin,
29
+ "installation": self.installation,
30
+ }
31
+
32
+ @classmethod
33
+ def from_dict(cls, d):
34
+ return cls(**d)
35
+
36
+
37
+ class FlatpakQuery:
38
+ def __init__(self, installation="user"):
39
+ self._install_flag = f"--{installation}"
40
+
41
+ def _run(self, args):
42
+ result = subprocess.run(
43
+ ["flatpak"] + args,
44
+ capture_output=True, text=True, check=True,
45
+ )
46
+ return result.stdout.strip()
47
+
48
+ def app_info(self, app_id):
49
+ raw = self._run(["info", self._install_flag, app_id])
50
+ info = {}
51
+ for line in raw.splitlines():
52
+ if ":" in line:
53
+ key, _, val = line.partition(":")
54
+ info[key.strip()] = val.strip()
55
+ return info
56
+
57
+ def app_ref(self, app_id):
58
+ info = self.app_info(app_id)
59
+ ref_str = info.get("Ref", "")
60
+ parts = ref_str.split("/")
61
+ return FlatpakRef(
62
+ app_id=parts[1] if len(parts) > 1 else app_id,
63
+ arch=parts[2] if len(parts) > 2 else "x86_64",
64
+ branch=parts[3] if len(parts) > 3 else "stable",
65
+ origin=info.get("Origin", "flathub"),
66
+ installation="user" if self._install_flag == "--user" else "system",
67
+ )
68
+
69
+ def runtime_ref(self, app_id):
70
+ raw = self._run(["info", self._install_flag, "--show-runtime", app_id])
71
+ parts = raw.split("/")
72
+ return FlatpakRef(
73
+ app_id=parts[0],
74
+ arch=parts[1] if len(parts) > 1 else "x86_64",
75
+ branch=parts[2] if len(parts) > 2 else "stable",
76
+ origin="flathub",
77
+ installation="user" if self._install_flag == "--user" else "system",
78
+ )
79
+
80
+ def repo_path(self, installation="user"):
81
+ if installation == "user":
82
+ return Path.home() / ".local/share/flatpak/repo"
83
+ return Path("/var/lib/flatpak/repo")
84
+
85
+
86
+ class BundleBuilder:
87
+ def __init__(self, query: FlatpakQuery):
88
+ self.query = query
89
+
90
+ def build_bundle(self, ref: FlatpakRef, output_path: Path, is_runtime=False):
91
+ repo = self.query.repo_path(ref.installation)
92
+ args = [
93
+ "flatpak", "build-bundle",
94
+ str(repo), str(output_path),
95
+ ref.app_id, ref.branch,
96
+ f"--arch={ref.arch}",
97
+ ]
98
+ if is_runtime:
99
+ args.append("--runtime")
100
+ subprocess.run(args, check=True)
101
+
102
+
103
+ class Backup:
104
+ MANIFEST = "flatback.yml"
105
+ DATA_DIR = "data"
106
+
107
+ def __init__(self, app_id, installation="user"):
108
+ self.app_id = app_id
109
+ self.installation = installation
110
+ self.query = FlatpakQuery(installation)
111
+ self.builder = BundleBuilder(self.query)
112
+
113
+ def _bundle_name(self, ref: FlatpakRef, is_runtime=False):
114
+ prefix = "runtime" if is_runtime else "app"
115
+ return f"{prefix}_{ref.app_id}_{ref.arch}_{ref.branch}.flatpak"
116
+
117
+ def _user_data_path(self):
118
+ return Path.home() / ".var" / "app" / self.app_id
119
+
120
+ def create(self, output_path=None, as_tar=True, include_data=False):
121
+ app_ref = self.query.app_ref(self.app_id)
122
+ runtime_ref = self.query.runtime_ref(self.app_id)
123
+ app_info = self.query.app_info(self.app_id)
124
+
125
+ with tempfile.TemporaryDirectory() as tmp:
126
+ tmp_dir = Path(tmp)
127
+
128
+ app_bundle = self._bundle_name(app_ref)
129
+ runtime_bundle = self._bundle_name(runtime_ref, is_runtime=True)
130
+
131
+ print(f"Bundling app: {app_ref.ref}")
132
+ self.builder.build_bundle(app_ref, tmp_dir / app_bundle)
133
+
134
+ print(f"Bundling runtime: {runtime_ref.ref}")
135
+ self.builder.build_bundle(runtime_ref, tmp_dir / runtime_bundle, is_runtime=True)
136
+
137
+ has_data = False
138
+ if include_data:
139
+ has_data = self._copy_user_data(tmp_dir)
140
+
141
+ manifest = {
142
+ "app": app_ref.as_dict(),
143
+ "runtime": runtime_ref.as_dict(),
144
+ "app_bundle": app_bundle,
145
+ "runtime_bundle": runtime_bundle,
146
+ "version": app_info.get("Version", "unknown"),
147
+ "flatback_version": __version__,
148
+ "has_data": has_data,
149
+ }
150
+
151
+ with open(tmp_dir / self.MANIFEST, "w") as f:
152
+ yaml.dump(manifest, f, default_flow_style=False)
153
+
154
+ default_name = self.app_id
155
+ if as_tar:
156
+ out = Path(output_path or f"{default_name}.tar.gz")
157
+ self._pack_tar(tmp_dir, out)
158
+ else:
159
+ out = Path(output_path or default_name)
160
+ if out.exists():
161
+ shutil.rmtree(out)
162
+ shutil.copytree(tmp_dir, out)
163
+
164
+ print(f"Backup saved to: {out}")
165
+ return out
166
+
167
+ def _copy_user_data(self, tmp_dir):
168
+ src = self._user_data_path()
169
+ if not src.exists():
170
+ print(f"No user data found at: {src}")
171
+ return False
172
+ dst = tmp_dir / self.DATA_DIR
173
+ print(f"Copying user data: {src}")
174
+ shutil.copytree(src, dst, symlinks=True)
175
+ return True
176
+
177
+ def _pack_tar(self, source_dir, tar_path):
178
+ with tarfile.open(tar_path, "w:gz") as tar:
179
+ for item in source_dir.iterdir():
180
+ tar.add(item, arcname=item.name)
181
+
182
+
183
+ class Restore:
184
+ MANIFEST = "flatback.yml"
185
+ DATA_DIR = "data"
186
+
187
+ def __init__(self, installation="user", restore_data=None):
188
+ self.installation = installation
189
+ self._install_flag = f"--{installation}"
190
+ self._restore_data = restore_data
191
+
192
+ def load(self, source_path):
193
+ source = Path(source_path)
194
+ if source.is_file() and (source.suffix == ".gz" or source.name.endswith(".tar.gz")):
195
+ self._load_from_tar(source)
196
+ elif source.is_dir():
197
+ self._load_from_dir(source)
198
+ else:
199
+ raise ValueError(f"Unsupported source: {source}")
200
+
201
+ def _load_from_tar(self, tar_path):
202
+ with tempfile.TemporaryDirectory() as tmp:
203
+ tmp_dir = Path(tmp)
204
+ with tarfile.open(tar_path, "r:gz") as tar:
205
+ tar.extractall(tmp_dir, filter="data")
206
+ self._load_from_dir(tmp_dir)
207
+
208
+ def _load_from_dir(self, directory):
209
+ manifest_path = directory / self.MANIFEST
210
+ if not manifest_path.exists():
211
+ raise FileNotFoundError(f"Manifest not found: {manifest_path}")
212
+
213
+ with open(manifest_path) as f:
214
+ manifest = yaml.safe_load(f)
215
+
216
+ runtime_bundle = directory / manifest["runtime_bundle"]
217
+ app_bundle = directory / manifest["app_bundle"]
218
+
219
+ runtime_ref = FlatpakRef.from_dict(manifest["runtime"])
220
+ app_ref = FlatpakRef.from_dict(manifest["app"])
221
+
222
+ print(f"Installing runtime: {runtime_ref.ref}")
223
+ self._install_bundle(runtime_bundle)
224
+
225
+ print(f"Installing app: {app_ref.ref}")
226
+ self._install_bundle(app_bundle)
227
+
228
+ if manifest.get("has_data"):
229
+ self._handle_user_data(directory, app_ref.app_id)
230
+
231
+ print(f"Restore complete: {app_ref.app_id}")
232
+
233
+ def _handle_user_data(self, directory, app_id):
234
+ data_dir = directory / self.DATA_DIR
235
+ if not data_dir.exists():
236
+ return
237
+
238
+ should_restore = self._restore_data
239
+ if should_restore is None:
240
+ answer = input("Backup contains user data. Restore it? [y/N] ").strip().lower()
241
+ should_restore = answer in ("y", "yes")
242
+
243
+ if not should_restore:
244
+ print("Skipping user data restore")
245
+ return
246
+
247
+ dst = Path.home() / ".var" / "app" / app_id
248
+ if dst.exists():
249
+ print(f"Replacing existing user data: {dst}")
250
+ shutil.rmtree(dst)
251
+ print(f"Restoring user data to: {dst}")
252
+ shutil.copytree(data_dir, dst, symlinks=True)
253
+
254
+ def _install_bundle(self, bundle_path):
255
+ result = subprocess.run(
256
+ ["flatpak", "install", self._install_flag, "--bundle", "-y", str(bundle_path)],
257
+ capture_output=True, text=True,
258
+ )
259
+ if result.returncode != 0:
260
+ if "already installed" in result.stderr:
261
+ print(f"Already installed, skipping: {bundle_path.name}")
262
+ else:
263
+ print(result.stderr)
264
+ raise subprocess.CalledProcessError(result.returncode, result.args)
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: flatback
3
+ Version: 1.0
4
+ Summary: Backup and restore Flatpak applications for offline transfer
5
+ Home-page: https://github.com/jfhack/flatback
6
+ Project-URL: Homepage, https://github.com/jfhack/flatback
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pyyaml
10
+ Dynamic: home-page
11
+
12
+ # Flatback
13
+
14
+ This is a simple script utility to back up and restore Flatpak applications for offline transfer.
15
+
16
+ Packages a Flatpak app together with its runtime into a single `.tar.gz` archive (or directory), including a YAML manifest. Ideal for moving apps to offline computers or preserving deprecated packages.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pip install flatback
22
+ ```
23
+
24
+ Requires `flatpak` to be installed on the system.
25
+
26
+ ## Usage
27
+
28
+ ### Backup
29
+
30
+ ```sh
31
+ flatback backup org.popular.emulator
32
+ flatback backup org.popular.emulator -o emulator-backup.tar.gz
33
+ flatback backup com.slack.Slack --no-tar
34
+ flatback backup com.google.Chrome --system
35
+ flatback backup org.popular.emulator --data
36
+ ```
37
+
38
+ ### Restore
39
+
40
+ ```sh
41
+ flatback restore org.popular.emulator.tar.gz
42
+ flatback restore emulator-backup.tar.gz
43
+ flatback restore com.slack.Slack/
44
+ flatback restore com.google.Chrome.tar.gz --system
45
+ flatback restore org.popular.emulator.tar.gz --data
46
+ flatback restore org.popular.emulator.tar.gz --no-data
47
+ ```
48
+
49
+ ## How It Works
50
+
51
+ **Backup** bundles the app and its runtime into `.flatpak` files using `flatpak build-bundle`, then packages everything with a `flatback.yml` manifest into a `.tar.gz`.
52
+
53
+ **Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
54
+
55
+ ### Manifest Example (`flatback.yml`)
56
+
57
+ ```yaml
58
+ app:
59
+ app_id: org.popular.emulator
60
+ arch: x86_64
61
+ branch: stable
62
+ installation: user
63
+ origin: flathub
64
+ app_bundle: app_org.popular.emulator_x86_64_stable.flatpak
65
+ flatback_version: '1.0'
66
+ has_data: true
67
+ runtime:
68
+ app_id: org.kde.Platform
69
+ arch: x86_64
70
+ branch: 5.15-23.08
71
+ installation: user
72
+ origin: flathub
73
+ runtime_bundle: runtime_org.kde.Platform_x86_64_5.15-23.08.flatpak
74
+ version: mainline-0-1734
75
+ ```
76
+
77
+ ## Arguments
78
+
79
+ ### `flatback backup`
80
+
81
+ | Argument | Description |
82
+ |-|-|
83
+ | `app_id` | Flatpak application ID (e.g. `org.popular.emulator`) |
84
+ | `-o`, `--output` | Output path (default: `<app_id>.tar.gz`) |
85
+ | `--no-tar` | Save as directory instead of tar.gz |
86
+ | `--data` | Include user data (`~/.var/app/<app_id>`) in backup |
87
+ | `--system` | Use system installation instead of user |
88
+
89
+ ### `flatback restore`
90
+
91
+ | Argument | Description |
92
+ |-|-|
93
+ | `source` | Path to backup `.tar.gz` or directory |
94
+ | `--data` | Restore user data without prompting |
95
+ | `--no-data` | Skip user data restore without prompting |
96
+ | `--system` | Install to system instead of user |
@@ -0,0 +1,8 @@
1
+ flatback/__init__.py,sha256=kM55qqhSrc4b-StNTuN03Srg0kwfg1G4BckCKhUytaE,20
2
+ flatback/__main__.py,sha256=f2t0CJ_zli1qxPPc5XKJc-RS-4O8TNgoedaG41qBtSw,1969
3
+ flatback/core.py,sha256=hR1kwG-YiSS8XXGO3lucy0bISrhkbULyh1MBd5wLoSc,8938
4
+ flatback-1.0.dist-info/METADATA,sha256=KNgcaOVGx0GV6DcJQ22q2oKtUYNsUGAJKzfs3N-qo60,2728
5
+ flatback-1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ flatback-1.0.dist-info/entry_points.txt,sha256=q-sGWJ9YozIuZLp0eF6u5b8Trys_eUmlx92_pkHxe4Y,52
7
+ flatback-1.0.dist-info/top_level.txt,sha256=2EfndshBwfueTm9DcMXk5MzsmJsJv2PBSISyAiZLUh4,9
8
+ flatback-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flatback = flatback.__main__:main
@@ -0,0 +1 @@
1
+ flatback