nxdev-org commited on
Commit
fcf7edb
·
verified ·
1 Parent(s): 2509e3a

Update sync_storage.py

Browse files
Files changed (1) hide show
  1. sync_storage.py +353 -68
sync_storage.py CHANGED
@@ -2,83 +2,231 @@
2
  import os
3
  import shutil
4
  import json
 
5
  from pathlib import Path
6
- from huggingface_hub import HfApi, create_repo
 
7
  import tarfile
8
  import tempfile
 
 
 
 
 
9
 
10
  class HFStorageSync:
11
- def __init__(self, repo_id, token=None, data_dir="/tmp/open-webui-data"):
 
12
  self.repo_id = repo_id
13
  self.data_dir = Path(data_dir)
14
  self.token = token
 
 
15
 
16
  # Initialize API with token directly
17
  self.api = HfApi(token=token) if token else HfApi()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def ensure_repo_exists(self):
20
  """Create repository if it doesn't exist"""
21
  if not self.token:
22
- print("No token provided, cannot create repository")
23
  return False
24
 
25
  try:
26
  # Check if repo exists
27
  repo_info = self.api.repo_info(repo_id=self.repo_id, repo_type="dataset")
28
- print(f"Repository {self.repo_id} exists")
29
  return True
30
  except Exception as e:
31
- print(f"Repository {self.repo_id} not found, attempting to create...")
32
  try:
33
  create_repo(
34
  repo_id=self.repo_id,
35
  repo_type="dataset",
36
  token=self.token,
37
- private=True, # Make it private by default
38
  exist_ok=True
39
  )
40
- print(f"Created repository {self.repo_id}")
41
 
42
- # Create initial README
43
- readme_content = """# Open WebUI Storage
 
 
 
 
 
 
 
 
44
 
45
- This dataset stores persistent data for Open WebUI deployment.
46
 
47
  ## Contents
48
 
49
- - `data.tar.gz`: Compressed archive containing all Open WebUI data including:
50
- - User configurations
51
- - Chat histories
52
- - Uploaded files
53
- - Database files
 
 
 
 
 
 
54
 
55
  This repository is automatically managed by the Open WebUI sync system.
56
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as tmp:
59
- tmp.write(readme_content)
60
- tmp.flush()
61
-
62
- self.api.upload_file(
63
- path_or_fileobj=tmp.name,
64
- path_in_repo="README.md",
65
- repo_id=self.repo_id,
66
- repo_type="dataset",
67
- commit_message="Initial repository setup",
68
- token=self.token
69
- )
70
-
71
- os.unlink(tmp.name)
72
-
73
- return True
74
- except Exception as create_error:
75
- print(f"Failed to create repository: {create_error}")
76
- return False
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def download_data(self):
79
- """Download and extract data from HF dataset repo"""
80
  try:
81
- print("Downloading data from Hugging Face...")
82
 
83
  # Ensure data directory exists and is writable
84
  self.data_dir.mkdir(parents=True, exist_ok=True)
@@ -88,25 +236,26 @@ This repository is automatically managed by the Open WebUI sync system.
88
  try:
89
  test_file.touch()
90
  test_file.unlink()
91
- print(f"Data directory {self.data_dir} is writable")
92
  except Exception as e:
93
- print(f"Warning: Data directory may not be writable: {e}")
94
  return
95
 
96
  if not self.token:
97
- print("No HF_TOKEN provided, skipping download")
98
  return
99
 
100
  # Ensure repository exists
101
  if not self.ensure_repo_exists():
102
- print("Could not access or create repository")
103
  return
104
 
105
- # Try to download the data archive
106
  try:
 
107
  file_path = self.api.hf_hub_download(
108
  repo_id=self.repo_id,
109
- filename="data.tar.gz",
110
  repo_type="dataset",
111
  token=self.token
112
  )
@@ -114,56 +263,154 @@ This repository is automatically managed by the Open WebUI sync system.
114
  with tarfile.open(file_path, 'r:gz') as tar:
115
  tar.extractall(self.data_dir)
116
 
117
- print(f"Data extracted to {self.data_dir}")
 
118
 
119
  except Exception as e:
120
- print(f"No existing data found (this is normal for first run): {e}")
121
 
122
  except Exception as e:
123
- print(f"Error during download: {e}")
124
 
125
- def upload_data(self):
126
- """Compress and upload data to HF dataset repo"""
127
  try:
128
  if not self.token:
129
- print("No HF_TOKEN provided, skipping upload")
130
  return
131
-
132
- print("Uploading data to Hugging Face...")
133
 
134
  if not self.data_dir.exists() or not any(self.data_dir.iterdir()):
135
- print("No data to upload")
 
 
 
 
 
 
 
 
 
 
 
136
  return
137
 
 
 
138
  # Ensure repository exists
139
  if not self.ensure_repo_exists():
140
- print("Could not access or create repository")
141
  return
142
 
143
- # Create temporary archive
 
 
 
 
144
  with tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) as tmp:
145
- with tarfile.open(tmp.name, 'w:gz') as tar:
 
146
  for item in self.data_dir.iterdir():
147
- if item.name not in ["test_write", ".gitkeep"]: # Skip test files
148
  tar.add(item, arcname=item.name)
 
 
 
 
 
 
 
149
 
150
- # Upload to HF
151
  self.api.upload_file(
152
  path_or_fileobj=tmp.name,
153
- path_in_repo="data.tar.gz",
154
  repo_id=self.repo_id,
155
  repo_type="dataset",
156
- commit_message="Update Open WebUI data",
157
  token=self.token
158
  )
159
 
160
- # Clean up
 
 
 
 
 
 
 
 
 
 
161
  os.unlink(tmp.name)
162
 
163
- print("Data uploaded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  except Exception as e:
166
- print(f"Error uploading data: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  def main():
169
  import sys
@@ -171,18 +418,56 @@ def main():
171
  repo_id = os.getenv("HF_STORAGE_REPO", "nxdev-org/open-webui-storage")
172
  token = os.getenv("HF_TOKEN")
173
  data_dir = os.getenv("DATA_DIR", "/tmp/open-webui-data")
 
174
 
175
- sync = HFStorageSync(repo_id, token, data_dir)
176
 
177
  if len(sys.argv) > 1:
178
- if sys.argv[1] == "download":
 
 
179
  sync.download_data()
180
- elif sys.argv[1] == "upload":
181
- sync.upload_data()
 
 
 
 
 
182
  else:
183
- print("Usage: sync_storage.py [download|upload]")
184
  else:
185
- print("Usage: sync_storage.py [download|upload]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  if __name__ == "__main__":
188
- main()
 
2
  import os
3
  import shutil
4
  import json
5
+ import hashlib
6
  from pathlib import Path
7
+ from datetime import datetime
8
+ from huggingface_hub import HfApi, create_repo, list_repo_files
9
  import tarfile
10
  import tempfile
11
+ import logging
12
+
13
+ # Set up logging
14
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
15
+ logger = logging.getLogger(__name__)
16
 
17
  class HFStorageSync:
18
+ def __init__(self, repo_id, token=None, data_dir="/tmp/open-webui-data",
19
+ max_backups=3, compression_level=6):
20
  self.repo_id = repo_id
21
  self.data_dir = Path(data_dir)
22
  self.token = token
23
+ self.max_backups = max_backups
24
+ self.compression_level = compression_level
25
 
26
  # Initialize API with token directly
27
  self.api = HfApi(token=token) if token else HfApi()
28
+
29
+ # File patterns for better organization
30
+ self.archive_pattern = "data-{timestamp}.tar.gz"
31
+ self.latest_link = "data-latest.tar.gz"
32
+ self.metadata_file = "storage-metadata.json"
33
+
34
+ def _get_directory_hash(self):
35
+ """Calculate hash of directory contents for change detection"""
36
+ hasher = hashlib.sha256()
37
+
38
+ if not self.data_dir.exists():
39
+ return hasher.hexdigest()
40
+
41
+ for item in sorted(self.data_dir.rglob('*')):
42
+ if item.is_file() and item.name not in [".gitkeep", "test_write"]:
43
+ hasher.update(str(item.relative_to(self.data_dir)).encode())
44
+ hasher.update(str(item.stat().st_mtime).encode())
45
+ hasher.update(str(item.stat().st_size).encode())
46
+
47
+ return hasher.hexdigest()
48
 
49
+ def _get_archive_size(self, archive_path):
50
+ """Get the size of an archive file"""
51
+ try:
52
+ return os.path.getsize(archive_path)
53
+ except:
54
+ return 0
55
+
56
+ def _format_size(self, size_bytes):
57
+ """Format file size in human readable format"""
58
+ for unit in ['B', 'KB', 'MB', 'GB']:
59
+ if size_bytes < 1024.0:
60
+ return f"{size_bytes:.1f} {unit}"
61
+ size_bytes /= 1024.0
62
+ return f"{size_bytes:.1f} TB"
63
+
64
  def ensure_repo_exists(self):
65
  """Create repository if it doesn't exist"""
66
  if not self.token:
67
+ logger.warning("No token provided, cannot create repository")
68
  return False
69
 
70
  try:
71
  # Check if repo exists
72
  repo_info = self.api.repo_info(repo_id=self.repo_id, repo_type="dataset")
73
+ logger.info(f"Repository {self.repo_id} exists")
74
  return True
75
  except Exception as e:
76
+ logger.info(f"Repository {self.repo_id} not found, attempting to create...")
77
  try:
78
  create_repo(
79
  repo_id=self.repo_id,
80
  repo_type="dataset",
81
  token=self.token,
82
+ private=True,
83
  exist_ok=True
84
  )
85
+ logger.info(f"Created repository {self.repo_id}")
86
 
87
+ # Create initial README and metadata
88
+ self._create_initial_files()
89
+ return True
90
+ except Exception as create_error:
91
+ logger.error(f"Failed to create repository: {create_error}")
92
+ return False
93
+
94
+ def _create_initial_files(self):
95
+ """Create initial repository files"""
96
+ readme_content = """# Open WebUI Storage
97
 
98
+ This dataset stores persistent data for Open WebUI deployment with automatic cleanup and versioning.
99
 
100
  ## Contents
101
 
102
+ - `data-latest.tar.gz`: Latest data archive (symlink)
103
+ - `data-YYYYMMDD-HHMMSS.tar.gz`: Timestamped data archives
104
+ - `storage-metadata.json`: Metadata about stored archives
105
+ - `README.md`: This file
106
+
107
+ ## Features
108
+
109
+ - Automatic cleanup of old backups
110
+ - Change detection to avoid unnecessary uploads
111
+ - Compression optimization
112
+ - Storage usage monitoring
113
 
114
  This repository is automatically managed by the Open WebUI sync system.
115
  """
116
+
117
+ metadata = {
118
+ "created": datetime.utcnow().isoformat(),
119
+ "max_backups": self.max_backups,
120
+ "archives": [],
121
+ "total_size": 0
122
+ }
123
+
124
+ # Upload README
125
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as tmp:
126
+ tmp.write(readme_content)
127
+ tmp.flush()
128
+
129
+ self.api.upload_file(
130
+ path_or_fileobj=tmp.name,
131
+ path_in_repo="README.md",
132
+ repo_id=self.repo_id,
133
+ repo_type="dataset",
134
+ commit_message="Initial repository setup",
135
+ token=self.token
136
+ )
137
+ os.unlink(tmp.name)
138
+
139
+ # Upload metadata
140
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
141
+ json.dump(metadata, tmp, indent=2)
142
+ tmp.flush()
143
+
144
+ self.api.upload_file(
145
+ path_or_fileobj=tmp.name,
146
+ path_in_repo=self.metadata_file,
147
+ repo_id=self.repo_id,
148
+ repo_type="dataset",
149
+ commit_message="Initial metadata",
150
+ token=self.token
151
+ )
152
+ os.unlink(tmp.name)
153
+
154
+ def _get_metadata(self):
155
+ """Download and parse metadata"""
156
+ try:
157
+ file_path = self.api.hf_hub_download(
158
+ repo_id=self.repo_id,
159
+ filename=self.metadata_file,
160
+ repo_type="dataset",
161
+ token=self.token
162
+ )
163
+
164
+ with open(file_path, 'r') as f:
165
+ return json.load(f)
166
+ except Exception as e:
167
+ logger.warning(f"Could not load metadata: {e}")
168
+ return {
169
+ "created": datetime.utcnow().isoformat(),
170
+ "max_backups": self.max_backups,
171
+ "archives": [],
172
+ "total_size": 0
173
+ }
174
+
175
+ def _update_metadata(self, metadata):
176
+ """Upload updated metadata"""
177
+ try:
178
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
179
+ json.dump(metadata, tmp, indent=2)
180
+ tmp.flush()
181
 
182
+ self.api.upload_file(
183
+ path_or_fileobj=tmp.name,
184
+ path_in_repo=self.metadata_file,
185
+ repo_id=self.repo_id,
186
+ repo_type="dataset",
187
+ commit_message="Update metadata",
188
+ token=self.token
189
+ )
190
+ os.unlink(tmp.name)
191
+ except Exception as e:
192
+ logger.error(f"Failed to update metadata: {e}")
 
 
 
 
 
 
 
 
193
 
194
+ def _cleanup_old_archives(self, metadata):
195
+ """Remove old archives beyond max_backups limit"""
196
+ if len(metadata["archives"]) <= self.max_backups:
197
+ return metadata
198
+
199
+ # Sort by timestamp, keep newest
200
+ archives = sorted(metadata["archives"], key=lambda x: x["timestamp"], reverse=True)
201
+ to_keep = archives[:self.max_backups]
202
+ to_delete = archives[self.max_backups:]
203
+
204
+ total_deleted_size = 0
205
+ for archive in to_delete:
206
+ try:
207
+ self.api.delete_file(
208
+ path_in_repo=archive["filename"],
209
+ repo_id=self.repo_id,
210
+ repo_type="dataset",
211
+ token=self.token
212
+ )
213
+ total_deleted_size += archive["size"]
214
+ logger.info(f"Deleted old archive: {archive['filename']} ({self._format_size(archive['size'])})")
215
+ except Exception as e:
216
+ logger.warning(f"Failed to delete {archive['filename']}: {e}")
217
+
218
+ metadata["archives"] = to_keep
219
+ metadata["total_size"] -= total_deleted_size
220
+
221
+ if total_deleted_size > 0:
222
+ logger.info(f"Cleaned up {self._format_size(total_deleted_size)} of storage")
223
+
224
+ return metadata
225
+
226
  def download_data(self):
227
+ """Download and extract latest data from HF dataset repo"""
228
  try:
229
+ logger.info("Downloading data from Hugging Face...")
230
 
231
  # Ensure data directory exists and is writable
232
  self.data_dir.mkdir(parents=True, exist_ok=True)
 
236
  try:
237
  test_file.touch()
238
  test_file.unlink()
239
+ logger.info(f"Data directory {self.data_dir} is writable")
240
  except Exception as e:
241
+ logger.warning(f"Data directory may not be writable: {e}")
242
  return
243
 
244
  if not self.token:
245
+ logger.warning("No HF_TOKEN provided, skipping download")
246
  return
247
 
248
  # Ensure repository exists
249
  if not self.ensure_repo_exists():
250
+ logger.error("Could not access or create repository")
251
  return
252
 
253
+ # Try to download the latest data archive
254
  try:
255
+ # First try the latest link
256
  file_path = self.api.hf_hub_download(
257
  repo_id=self.repo_id,
258
+ filename=self.latest_link,
259
  repo_type="dataset",
260
  token=self.token
261
  )
 
263
  with tarfile.open(file_path, 'r:gz') as tar:
264
  tar.extractall(self.data_dir)
265
 
266
+ archive_size = self._get_archive_size(file_path)
267
+ logger.info(f"Data extracted to {self.data_dir} ({self._format_size(archive_size)})")
268
 
269
  except Exception as e:
270
+ logger.info(f"No existing data found (normal for first run): {e}")
271
 
272
  except Exception as e:
273
+ logger.error(f"Error during download: {e}")
274
 
275
+ def upload_data(self, force=False):
276
+ """Compress and upload data to HF dataset repo with change detection"""
277
  try:
278
  if not self.token:
279
+ logger.warning("No HF_TOKEN provided, skipping upload")
280
  return
 
 
281
 
282
  if not self.data_dir.exists() or not any(self.data_dir.iterdir()):
283
+ logger.warning("No data to upload")
284
+ return
285
+
286
+ # Calculate current directory hash
287
+ current_hash = self._get_directory_hash()
288
+
289
+ # Get metadata to check for changes
290
+ metadata = self._get_metadata()
291
+ last_hash = metadata.get("last_hash")
292
+
293
+ if not force and current_hash == last_hash:
294
+ logger.info("No changes detected, skipping upload")
295
  return
296
 
297
+ logger.info("Changes detected, preparing upload...")
298
+
299
  # Ensure repository exists
300
  if not self.ensure_repo_exists():
301
+ logger.error("Could not access or create repository")
302
  return
303
 
304
+ # Create timestamped filename
305
+ timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
306
+ archive_filename = self.archive_pattern.format(timestamp=timestamp)
307
+
308
+ # Create temporary archive with optimized compression
309
  with tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) as tmp:
310
+ with tarfile.open(tmp.name, f'w:gz', compresslevel=self.compression_level) as tar:
311
+ total_files = 0
312
  for item in self.data_dir.iterdir():
313
+ if item.name not in ["test_write", ".gitkeep"]:
314
  tar.add(item, arcname=item.name)
315
+ if item.is_file():
316
+ total_files += 1
317
+ elif item.is_dir():
318
+ total_files += sum(1 for _ in item.rglob('*') if _.is_file())
319
+
320
+ archive_size = self._get_archive_size(tmp.name)
321
+ logger.info(f"Created archive: {self._format_size(archive_size)}, {total_files} files")
322
 
323
+ # Upload timestamped archive
324
  self.api.upload_file(
325
  path_or_fileobj=tmp.name,
326
+ path_in_repo=archive_filename,
327
  repo_id=self.repo_id,
328
  repo_type="dataset",
329
+ commit_message=f"Update Open WebUI data - {timestamp}",
330
  token=self.token
331
  )
332
 
333
+ # Upload as latest (overwrite)
334
+ self.api.upload_file(
335
+ path_or_fileobj=tmp.name,
336
+ path_in_repo=self.latest_link,
337
+ repo_id=self.repo_id,
338
+ repo_type="dataset",
339
+ commit_message=f"Update latest data - {timestamp}",
340
+ token=self.token
341
+ )
342
+
343
+ # Clean up temp file
344
  os.unlink(tmp.name)
345
 
346
+ # Update metadata
347
+ archive_info = {
348
+ "filename": archive_filename,
349
+ "timestamp": timestamp,
350
+ "size": archive_size,
351
+ "hash": current_hash,
352
+ "files_count": total_files
353
+ }
354
+
355
+ metadata["archives"].append(archive_info)
356
+ metadata["total_size"] += archive_size
357
+ metadata["last_hash"] = current_hash
358
+ metadata["last_upload"] = datetime.utcnow().isoformat()
359
+
360
+ # Cleanup old archives
361
+ metadata = self._cleanup_old_archives(metadata)
362
+
363
+ # Update metadata
364
+ self._update_metadata(metadata)
365
+
366
+ logger.info(f"Upload successful: {archive_filename} ({self._format_size(archive_size)})")
367
+ logger.info(f"Total storage used: {self._format_size(metadata['total_size'])}")
368
 
369
  except Exception as e:
370
+ logger.error(f"Error uploading data: {e}")
371
+
372
+ def list_archives(self):
373
+ """List all available archives"""
374
+ try:
375
+ metadata = self._get_metadata()
376
+
377
+ if not metadata["archives"]:
378
+ logger.info("No archives found")
379
+ return
380
+
381
+ logger.info("Available archives:")
382
+ logger.info("-" * 60)
383
+
384
+ total_size = 0
385
+ for archive in sorted(metadata["archives"], key=lambda x: x["timestamp"], reverse=True):
386
+ size_str = self._format_size(archive["size"])
387
+ files_count = archive.get("files_count", "unknown")
388
+ logger.info(f"{archive['filename']:<30} {size_str:>10} {files_count:>6} files")
389
+ total_size += archive["size"]
390
+
391
+ logger.info("-" * 60)
392
+ logger.info(f"Total: {len(metadata['archives'])} archives, {self._format_size(total_size)}")
393
+
394
+ except Exception as e:
395
+ logger.error(f"Error listing archives: {e}")
396
+
397
+ def cleanup_storage(self):
398
+ """Force cleanup of old archives"""
399
+ try:
400
+ metadata = self._get_metadata()
401
+ old_size = metadata["total_size"]
402
+
403
+ metadata = self._cleanup_old_archives(metadata)
404
+ self._update_metadata(metadata)
405
+
406
+ saved = old_size - metadata["total_size"]
407
+ if saved > 0:
408
+ logger.info(f"Cleanup completed. Saved {self._format_size(saved)} of storage")
409
+ else:
410
+ logger.info("No cleanup needed")
411
+
412
+ except Exception as e:
413
+ logger.error(f"Error during cleanup: {e}")
414
 
415
  def main():
416
  import sys
 
418
  repo_id = os.getenv("HF_STORAGE_REPO", "nxdev-org/open-webui-storage")
419
  token = os.getenv("HF_TOKEN")
420
  data_dir = os.getenv("DATA_DIR", "/tmp/open-webui-data")
421
+ max_backups = int(os.getenv("MAX_BACKUPS", "3"))
422
 
423
+ sync = HFStorageSync(repo_id, token, data_dir, max_backups=max_backups)
424
 
425
  if len(sys.argv) > 1:
426
+ command = sys.argv[1].lower()
427
+
428
+ if command == "download":
429
  sync.download_data()
430
+ elif command == "upload":
431
+ force = len(sys.argv) > 2 and sys.argv[2] == "--force"
432
+ sync.upload_data(force=force)
433
+ elif command == "list":
434
+ sync.list_archives()
435
+ elif command == "cleanup":
436
+ sync.cleanup_storage()
437
  else:
438
+ print("Usage: sync_storage.py [download|upload [--force]|list|cleanup]")
439
  else:
440
+ print("Usage: sync_storage.py [download|upload [--force]|list|cleanup]")
441
+ print("\nCommands:")
442
+ print(" download Download and extract latest data")
443
+ print(" upload Upload data (only if changed)")
444
+ print(" upload --force Force upload even if no changes detected")
445
+ print(" list List all available archives")
446
+ print(" cleanup Force cleanup of old archives")
447
+ print("\nEnvironment variables:")
448
+ print(" HF_STORAGE_REPO Hugging Face repository ID")
449
+ print(" HF_TOKEN Hugging Face API token")
450
+ print(" DATA_DIR Local data directory")
451
+ print(" MAX_BACKUPS Maximum number of backups to keep (default: 3)")
452
+
453
+ """
454
+ # Download latest data
455
+ python sync_storage.py download
456
 
457
+ # Upload only if changed (default)
458
+ python sync_storage.py upload
459
+
460
+ # Force upload regardless of changes
461
+ python sync_storage.py upload --force
462
+
463
+ # List all archives
464
+ python sync_storage.py list
465
+
466
+ # Clean up old archives
467
+ python sync_storage.py cleanup
468
+
469
+ # Set custom backup limit
470
+ MAX_BACKUPS=5 python sync_storage.py upload
471
+ """
472
  if __name__ == "__main__":
473
+ main()