gd-storage 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 c4k3ss
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,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: gd-storage
3
+ Version: 1.0.0
4
+ Summary: Encode files into Geometry Dash levels
5
+ Author-email: c4k3ss <iustin@mail.c4k3ss.dev>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/c4k3ss/GD-Storage
8
+ Project-URL: Repository, https://github.com/c4k3ss/GD-Storage
9
+ Project-URL: Issues, https://github.com/c4k3ss/GD-Storage/issues
10
+ Keywords: geometry-dash,gd,encoding,steganography,cli
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Games/Entertainment
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: dashlib
26
+ Requires-Dist: pycryptodomex
27
+ Dynamic: license-file
28
+
29
+ # GD Storage
30
+
31
+
32
+
33
+ ## What is this?
34
+
35
+ GD Storage lets you hide any file inside a Geometry Dash level by encoding the file's binary data into level object properties. The encoded level can be:
36
+ - Saved to your local GD save file
37
+ - Uploaded to GD servers and shared via level ID
38
+ - Downloaded and decoded back to the original file
39
+
40
+ ## Features
41
+
42
+ - **Upload to GD Servers** - Store files on RobTop's servers, get a level ID to share
43
+ - **Fetch from GD Servers** - Download and decode files using just a level ID
44
+ - **Local Save Support** - Inject encoded levels directly into your GD save file
45
+ - **Cross-Platform** - Works on Windows, macOS, and Linux (via Proton)
46
+ - **Configurable** - Custom GD save paths for non-standard installations
47
+ - **Secure** - Path traversal protection, input validation, overwrite confirmation
48
+
49
+ ## Installation
50
+
51
+ Requires Python 3.10+
52
+
53
+ ```bash
54
+ pip install .
55
+ ```
56
+
57
+ Or install in development mode:
58
+
59
+ ```bash
60
+ pip install -e .
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ```bash
66
+ # Upload a file to GD servers
67
+ gd-storage --upload photo.png
68
+
69
+ # Download and decode from GD servers
70
+ gd-storage --fetch 123456789
71
+
72
+ # Encode and inject into local GD save
73
+ gd-storage --encode document.pdf
74
+
75
+ # Decode from local GD save
76
+ gd-storage --decode "LevelName"
77
+
78
+ # Configure GD save path (for non-standard installations)
79
+ gd-storage --config
80
+ ```
81
+
82
+ ## How It Works
83
+
84
+ Files are encoded using a base-9999 group encoding method:
85
+
86
+ 1. **Compression** - The file is gzip compressed with the filename prepended
87
+ 2. **Chunking** - Binary data is split into 8-byte chunks
88
+ 3. **Base Conversion** - Each chunk is converted to 5 base-9999 numbers (1-9999)
89
+ 4. **Object Encoding** - Numbers are stored in GD object group properties (property 57)
90
+ 5. **Level String** - Objects are serialized into a valid GD level string
91
+
92
+
93
+ ### Why Base-9999?
94
+
95
+ GD supports up to 9999 groups per object. By treating group numbers as digits in a base-9999 number system, we can efficiently pack 8 bytes into just 5 group values, achieving ~62.5% space efficiency.
96
+
97
+ ## Configuration
98
+
99
+ On first run, GD Storage auto-detects your save path:
100
+ - **Windows**: `%LOCALAPPDATA%\GeometryDash`
101
+ - **macOS**: `~/Library/Application Support/GeometryDash`
102
+
103
+ For custom paths (Linux/Proton, custom installs), run:
104
+
105
+ ```bash
106
+ gd-storage --config
107
+ ```
108
+
109
+ Config is saved to `~/.config/gd-storage/config.json`
110
+
111
+ ## Platform Support
112
+
113
+ | Platform | Local Save | Upload/Fetch |
114
+ |----------|------------|--------------|
115
+ | Windows | Yes | Yes |
116
+ | macOS | Yes | Yes |
117
+ | Linux | Yes* | Yes |
118
+
119
+ *Linux requires configuring the Proton GD save path manually
120
+
121
+
122
+ ## Dependencies
123
+
124
+ - [dashlib](https://pypi.org/project/dashlib/) - GD server API
125
+ - [pycryptodomex](https://pypi.org/project/pycryptodomex/) - AES encryption for macOS saves
126
+
127
+ ## License
128
+
129
+ GD Storage is licensed under the MIT License
130
+
131
+ ## Disclaimer
132
+
133
+ This project is for educational purposes. Use responsibly and don't abuse GD's servers.
134
+
135
+ > GitHub [@c4k3ss](https://github.com/c4k3ss) &nbsp;&middot;&nbsp;
136
+ > Twitter [@_c4k3ss](https://x.com/_c4k3ss)
@@ -0,0 +1,108 @@
1
+ # GD Storage
2
+
3
+
4
+
5
+ ## What is this?
6
+
7
+ GD Storage lets you hide any file inside a Geometry Dash level by encoding the file's binary data into level object properties. The encoded level can be:
8
+ - Saved to your local GD save file
9
+ - Uploaded to GD servers and shared via level ID
10
+ - Downloaded and decoded back to the original file
11
+
12
+ ## Features
13
+
14
+ - **Upload to GD Servers** - Store files on RobTop's servers, get a level ID to share
15
+ - **Fetch from GD Servers** - Download and decode files using just a level ID
16
+ - **Local Save Support** - Inject encoded levels directly into your GD save file
17
+ - **Cross-Platform** - Works on Windows, macOS, and Linux (via Proton)
18
+ - **Configurable** - Custom GD save paths for non-standard installations
19
+ - **Secure** - Path traversal protection, input validation, overwrite confirmation
20
+
21
+ ## Installation
22
+
23
+ Requires Python 3.10+
24
+
25
+ ```bash
26
+ pip install .
27
+ ```
28
+
29
+ Or install in development mode:
30
+
31
+ ```bash
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ # Upload a file to GD servers
39
+ gd-storage --upload photo.png
40
+
41
+ # Download and decode from GD servers
42
+ gd-storage --fetch 123456789
43
+
44
+ # Encode and inject into local GD save
45
+ gd-storage --encode document.pdf
46
+
47
+ # Decode from local GD save
48
+ gd-storage --decode "LevelName"
49
+
50
+ # Configure GD save path (for non-standard installations)
51
+ gd-storage --config
52
+ ```
53
+
54
+ ## How It Works
55
+
56
+ Files are encoded using a base-9999 group encoding method:
57
+
58
+ 1. **Compression** - The file is gzip compressed with the filename prepended
59
+ 2. **Chunking** - Binary data is split into 8-byte chunks
60
+ 3. **Base Conversion** - Each chunk is converted to 5 base-9999 numbers (1-9999)
61
+ 4. **Object Encoding** - Numbers are stored in GD object group properties (property 57)
62
+ 5. **Level String** - Objects are serialized into a valid GD level string
63
+
64
+
65
+ ### Why Base-9999?
66
+
67
+ GD supports up to 9999 groups per object. By treating group numbers as digits in a base-9999 number system, we can efficiently pack 8 bytes into just 5 group values, achieving ~62.5% space efficiency.
68
+
69
+ ## Configuration
70
+
71
+ On first run, GD Storage auto-detects your save path:
72
+ - **Windows**: `%LOCALAPPDATA%\GeometryDash`
73
+ - **macOS**: `~/Library/Application Support/GeometryDash`
74
+
75
+ For custom paths (Linux/Proton, custom installs), run:
76
+
77
+ ```bash
78
+ gd-storage --config
79
+ ```
80
+
81
+ Config is saved to `~/.config/gd-storage/config.json`
82
+
83
+ ## Platform Support
84
+
85
+ | Platform | Local Save | Upload/Fetch |
86
+ |----------|------------|--------------|
87
+ | Windows | Yes | Yes |
88
+ | macOS | Yes | Yes |
89
+ | Linux | Yes* | Yes |
90
+
91
+ *Linux requires configuring the Proton GD save path manually
92
+
93
+
94
+ ## Dependencies
95
+
96
+ - [dashlib](https://pypi.org/project/dashlib/) - GD server API
97
+ - [pycryptodomex](https://pypi.org/project/pycryptodomex/) - AES encryption for macOS saves
98
+
99
+ ## License
100
+
101
+ GD Storage is licensed under the MIT License
102
+
103
+ ## Disclaimer
104
+
105
+ This project is for educational purposes. Use responsibly and don't abuse GD's servers.
106
+
107
+ > GitHub [@c4k3ss](https://github.com/c4k3ss) &nbsp;&middot;&nbsp;
108
+ > Twitter [@_c4k3ss](https://x.com/_c4k3ss)
@@ -0,0 +1,389 @@
1
+ """
2
+ GD Storage - Encode files into Geometry Dash levels
3
+ """
4
+ import argparse
5
+ import sys
6
+ import os
7
+ import base64
8
+ import gzip
9
+ import getpass
10
+ import re
11
+ import json
12
+ import platform
13
+ from pathlib import Path
14
+
15
+ from methods import METHODS
16
+
17
+
18
+ # Config file location
19
+ CONFIG_DIR = Path.home() / ".config" / "gd-storage"
20
+ CONFIG_FILE = CONFIG_DIR / "config.json"
21
+
22
+
23
+ def load_config() -> dict:
24
+ """Load config from file or return defaults."""
25
+ if CONFIG_FILE.exists():
26
+ try:
27
+ return json.loads(CONFIG_FILE.read_text())
28
+ except (json.JSONDecodeError, OSError):
29
+ pass
30
+ return {}
31
+
32
+
33
+ def save_config(config: dict):
34
+ """Save config to file."""
35
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
36
+ CONFIG_FILE.write_text(json.dumps(config, indent=2))
37
+
38
+
39
+ def get_default_gd_path() -> str | None:
40
+ """Get default GD save path for current platform."""
41
+ system = platform.system()
42
+ if system == "Windows":
43
+ local_app_data = os.getenv("LOCALAPPDATA")
44
+ if local_app_data:
45
+ return os.path.join(local_app_data, "GeometryDash")
46
+ elif system == "Darwin": # macOS
47
+ return os.path.expanduser("~/Library/Application Support/GeometryDash")
48
+ return None
49
+
50
+
51
+ def get_manager(config: dict):
52
+ """Get GD save manager based on config."""
53
+ from save_manager import new_manager
54
+
55
+ gd_path = config.get("gd_path")
56
+ gd_format = config.get("format", "auto")
57
+
58
+ # Auto-detect path if not configured
59
+ if not gd_path:
60
+ gd_path = get_default_gd_path()
61
+ if not gd_path:
62
+ raise ValueError("GD path not configured. Run: gd-storage --config")
63
+
64
+ # Check path exists
65
+ if not os.path.exists(gd_path):
66
+ raise ValueError(f"GD save folder not found: {gd_path}\nRun: gd-storage --config")
67
+
68
+ return new_manager(path=gd_path, format=gd_format)
69
+
70
+
71
+ def cmd_config():
72
+ """Configure GD storage settings."""
73
+ config = load_config()
74
+
75
+ print("GD Storage Configuration")
76
+ print("=" * 40)
77
+
78
+ # Show current settings
79
+ current_path = config.get("gd_path", get_default_gd_path() or "Not set")
80
+ current_format = config.get("format", "auto")
81
+ print(f"Current path: {current_path}")
82
+ print(f"Current format: {current_format}")
83
+ print()
84
+
85
+ # Get new path
86
+ print("Enter GD save folder path (or press Enter to keep current):")
87
+ print(" Windows: %LOCALAPPDATA%\\GeometryDash")
88
+ print(" macOS: ~/Library/Application Support/GeometryDash")
89
+ print(" Linux/Proton: ~/.steam/steam/steamapps/compatdata/.../GeometryDash")
90
+ new_path = input("> ").strip()
91
+
92
+ if new_path:
93
+ new_path = os.path.expanduser(os.path.expandvars(new_path))
94
+ if not os.path.exists(new_path):
95
+ print(f"Warning: Path does not exist: {new_path}")
96
+ confirm = input("Save anyway? (y/N): ").strip().lower()
97
+ if confirm != 'y':
98
+ return 1
99
+ config["gd_path"] = new_path
100
+
101
+ # Get format
102
+ print()
103
+ print("Save format:")
104
+ print(" 1. auto (detect from platform)")
105
+ print(" 2. windows (XOR encryption - Windows/Linux/Proton)")
106
+ print(" 3. mac (AES encryption - macOS)")
107
+ choice = input("Choice [1]: ").strip() or "1"
108
+
109
+ if choice == "2":
110
+ config["format"] = "windows"
111
+ elif choice == "3":
112
+ config["format"] = "mac"
113
+ else:
114
+ config["format"] = "auto"
115
+
116
+ save_config(config)
117
+ print()
118
+ print(f"Config saved to {CONFIG_FILE}")
119
+ return 0
120
+
121
+
122
+ def save_decoded_file(filename: str, data: bytes) -> Path | None:
123
+ """Save decoded file to Downloads, checking for overwrites."""
124
+ # Sanitize filename - prevent path traversal
125
+ safe_filename = Path(filename).name
126
+ if not safe_filename:
127
+ safe_filename = "decoded_file"
128
+
129
+ downloads = Path(os.path.expanduser("~")) / "Downloads" / safe_filename
130
+
131
+ if downloads.exists():
132
+ print(f"File already exists: {downloads}")
133
+ choice = input("Overwrite? (y/N): ").strip().lower()
134
+ if choice != 'y':
135
+ # Try adding a number suffix
136
+ stem = downloads.stem
137
+ suffix = downloads.suffix
138
+ for i in range(1, 100):
139
+ alt = downloads.parent / f"{stem}_{i}{suffix}"
140
+ if not alt.exists():
141
+ downloads = alt
142
+ break
143
+ else:
144
+ print("Could not find available filename")
145
+ return None
146
+
147
+ downloads.write_bytes(data)
148
+ return downloads
149
+
150
+
151
+ def show_help():
152
+ print("GD Storage - Encode files into Geometry Dash levels")
153
+ print()
154
+ print("Usage:")
155
+ print(" gd-storage --upload <filepath> Encode and upload to GD servers")
156
+ print(" gd-storage --fetch <level_id> Download and decode from GD servers")
157
+ print(" gd-storage --encode <filepath> Encode and inject into local GD save")
158
+ print(" gd-storage --decode <levelname> Decode from local GD save")
159
+ print(" gd-storage --config Configure GD save path")
160
+
161
+
162
+ def cmd_upload(filepath: Path, encode_func):
163
+ """Encode and upload a file to GD servers."""
164
+ import dashlib
165
+
166
+ if not filepath.exists():
167
+ print(f"File not found: {filepath}")
168
+ return 1
169
+
170
+ print(f"Encoding {filepath.name} ({filepath.stat().st_size:,} bytes)...")
171
+ level_str = encode_func(filepath)
172
+
173
+ # Decompress to get raw level for object count
174
+ if level_str.startswith('H4sI'):
175
+ compressed = base64.urlsafe_b64decode(level_str + '==')
176
+ raw_level = gzip.decompress(compressed).decode('utf-8')
177
+ else:
178
+ raw_level = level_str
179
+
180
+ obj_count = raw_level.count(';')
181
+ level_name = filepath.stem[:20]
182
+ description = f"Encoded: {filepath.name}"
183
+ desc_encoded = base64.urlsafe_b64encode(description.encode()).decode()
184
+
185
+ class UploadLevel:
186
+ def __init__(self):
187
+ self.levelString = level_str
188
+ self.levelName = level_name
189
+ self.description = desc_encoded
190
+ self.password = 1
191
+ self.copiedId = 0
192
+ self.version = 1
193
+ self.officialSong = 0
194
+ self.customSongID = 0
195
+ self.coins = 0
196
+ self.lowDetailMode = False
197
+ self.objects = obj_count
198
+ self.isAuto = False
199
+ self.twoPlayer = False
200
+ self.starsRequested = 0
201
+ self.editorTime = 0
202
+ self.editorTimeCopies = 0
203
+ self.length = dashlib.LENGTH_TINY
204
+
205
+ level = UploadLevel()
206
+
207
+ # Get credentials
208
+ username, account_id, gjp2 = get_credentials()
209
+ if not username:
210
+ return 1
211
+
212
+ print(f"Uploading '{level_name}'...")
213
+ try:
214
+ result = dashlib.uploadLevel(
215
+ level=level,
216
+ username=username,
217
+ accountID=account_id,
218
+ gjp2=gjp2,
219
+ )
220
+ if result == "-1" or result.startswith("-"):
221
+ raise ValueError(f"Server returned: {result}")
222
+ new_level_id = int(result)
223
+ print(f"Uploaded! Level ID: {new_level_id}")
224
+ print(f"Fetch with: gd-storage --fetch {new_level_id}")
225
+ return 0
226
+ except Exception as e:
227
+ print(f"Upload failed: {e}")
228
+ return 1
229
+
230
+
231
+ def cmd_fetch(level_id: int, decode_func):
232
+ """Download and decode a level from GD servers."""
233
+ from gd_api import download_level
234
+
235
+ print(f"Fetching level {level_id}...")
236
+ try:
237
+ level_data = download_level(level_id)
238
+ except Exception as e:
239
+ print(f"Failed to fetch: {e}")
240
+ return 1
241
+
242
+ level_name = level_data.get("name", "Unknown")
243
+ description = level_data.get("description", "")
244
+ level_str = level_data.get("level_string", "")
245
+
246
+ print(f"Level: {level_name}")
247
+ if description:
248
+ print(f"Description: {description}")
249
+
250
+ try:
251
+ filename, data = decode_func(level_str)
252
+ saved_path = save_decoded_file(filename, data)
253
+ if saved_path:
254
+ print(f"Saved to {saved_path} ({len(data):,} bytes)")
255
+ return 0
256
+ return 1
257
+ except Exception as e:
258
+ print(f"Failed to decode: {e}")
259
+ return 1
260
+
261
+
262
+ def cmd_encode(filepath: Path, encode_func):
263
+ """Encode and inject into local GD save."""
264
+ if not filepath.exists():
265
+ print(f"File not found: {filepath}")
266
+ return 1
267
+
268
+ print(f"Encoding {filepath.name} ({filepath.stat().st_size:,} bytes)...")
269
+ level_str = encode_func(filepath)
270
+
271
+ config = load_config()
272
+ try:
273
+ manager = get_manager(config)
274
+ except ValueError as e:
275
+ print(f"Error: {e}")
276
+ return 1
277
+
278
+ manager.injectLevel(level_str, filepath.stem, f"Encoded: {filepath.name}")
279
+ manager.save(ccll=True, ccgm=False)
280
+ print(f"Injected as '{filepath.stem}'")
281
+ return 0
282
+
283
+
284
+ def cmd_decode(level_name: str, decode_func):
285
+ """Decode from local GD save."""
286
+ print(f"Extracting '{level_name}'...")
287
+
288
+ config = load_config()
289
+ try:
290
+ manager = get_manager(config)
291
+ except ValueError as e:
292
+ print(f"Error: {e}")
293
+ return 1
294
+
295
+ ccll = manager.ccll.decode('utf-8', errors='ignore')
296
+
297
+ start = ccll.find(f'<s>{level_name}</s>')
298
+ if start == -1:
299
+ print(f"Level '{level_name}' not found!")
300
+ return 1
301
+
302
+ k4_start = ccll.find('<k>k4</k><s>', start) + len('<k>k4</k><s>')
303
+ k4_end = ccll.find('</s>', k4_start)
304
+ level_str = ccll[k4_start:k4_end]
305
+
306
+ try:
307
+ filename, data = decode_func(level_str)
308
+ saved_path = save_decoded_file(filename, data)
309
+ if saved_path:
310
+ print(f"Saved to {saved_path} ({len(data):,} bytes)")
311
+ return 0
312
+ return 1
313
+ except Exception as e:
314
+ print(f"Failed to decode: {e}")
315
+ return 1
316
+
317
+
318
+ def get_credentials():
319
+ """Get GD credentials from save file or prompt user."""
320
+ from gd_api import get_account_id
321
+
322
+ config = load_config()
323
+ try:
324
+ manager = get_manager(config)
325
+ ccgm = manager.ccgm.decode('utf-8', errors='ignore')
326
+
327
+ saved_user = re.search(r'<k>GJA_001</k><s>([^<]+)</s>', ccgm)
328
+ saved_id = re.search(r'<k>GJA_003</k><i>(\d+)</i>', ccgm)
329
+ saved_gjp2 = re.search(r'<k>GJA_005</k><s>([^<]+)</s>', ccgm)
330
+
331
+ if saved_user and saved_id and saved_gjp2:
332
+ print(f"Using saved credentials: {saved_user.group(1)}")
333
+ return saved_user.group(1), int(saved_id.group(1)), saved_gjp2.group(1)
334
+ except ValueError:
335
+ pass # No local save, prompt for credentials
336
+
337
+ print("Enter GD credentials:")
338
+ username = input("Username: ")
339
+ password = getpass.getpass("Password: ")
340
+
341
+ try:
342
+ account_id, gjp2 = get_account_id(username, password)
343
+ print(f"Logged in as {username}")
344
+ return username, account_id, gjp2
345
+ except Exception as e:
346
+ print(f"Login failed: {e}")
347
+ return None, None, None
348
+
349
+
350
+ def main():
351
+ parser = argparse.ArgumentParser(
352
+ description="GD Storage - Encode files into Geometry Dash levels",
353
+ add_help=False
354
+ )
355
+ parser.add_argument('--upload', metavar='FILE', help='Encode and upload to GD servers')
356
+ parser.add_argument('--fetch', metavar='ID', type=int, help='Download and decode from GD servers')
357
+ parser.add_argument('--encode', metavar='FILE', help='Encode and inject into local GD save')
358
+ parser.add_argument('--decode', metavar='NAME', help='Decode from local GD save')
359
+ parser.add_argument('--config', action='store_true', help='Configure GD save path')
360
+ parser.add_argument('--help', '-h', action='store_true', help='Show help')
361
+
362
+ args = parser.parse_args()
363
+
364
+ # Handle config
365
+ if args.config:
366
+ return cmd_config()
367
+
368
+ # Show help if no args or --help
369
+ if args.help or (not args.upload and not args.fetch and not args.encode and not args.decode):
370
+ show_help()
371
+ return 0
372
+
373
+ encode_func, decode_func, _ = METHODS[6]
374
+
375
+ # Run command
376
+ if args.upload:
377
+ return cmd_upload(Path(args.upload), encode_func)
378
+ elif args.fetch:
379
+ return cmd_fetch(args.fetch, decode_func)
380
+ elif args.encode:
381
+ return cmd_encode(Path(args.encode), encode_func)
382
+ elif args.decode:
383
+ return cmd_decode(args.decode, decode_func)
384
+
385
+ return 0
386
+
387
+
388
+ if __name__ == "__main__":
389
+ sys.exit(main())