hippius 0.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.
- hippius-0.1.0.dist-info/METADATA +475 -0
- hippius-0.1.0.dist-info/RECORD +9 -0
- hippius-0.1.0.dist-info/WHEEL +4 -0
- hippius-0.1.0.dist-info/entry_points.txt +4 -0
- hippius_sdk/__init__.py +11 -0
- hippius_sdk/cli.py +658 -0
- hippius_sdk/client.py +246 -0
- hippius_sdk/ipfs.py +985 -0
- hippius_sdk/substrate.py +688 -0
hippius_sdk/cli.py
ADDED
@@ -0,0 +1,658 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Command Line Interface tools for Hippius SDK.
|
4
|
+
|
5
|
+
This module provides CLI tools for working with the Hippius SDK, including
|
6
|
+
utilities for encryption key generation, file operations, and marketplace interactions.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import base64
|
10
|
+
import argparse
|
11
|
+
import os
|
12
|
+
import sys
|
13
|
+
import time
|
14
|
+
from typing import Optional, List
|
15
|
+
|
16
|
+
# Import SDK components
|
17
|
+
from hippius_sdk import HippiusClient
|
18
|
+
from hippius_sdk.substrate import FileInput
|
19
|
+
from dotenv import load_dotenv
|
20
|
+
|
21
|
+
try:
|
22
|
+
import nacl.utils
|
23
|
+
import nacl.secret
|
24
|
+
except ImportError:
|
25
|
+
ENCRYPTION_AVAILABLE = False
|
26
|
+
else:
|
27
|
+
ENCRYPTION_AVAILABLE = True
|
28
|
+
|
29
|
+
# Load environment variables
|
30
|
+
load_dotenv()
|
31
|
+
|
32
|
+
|
33
|
+
def generate_key():
|
34
|
+
"""Generate a random encryption key for NaCl secretbox."""
|
35
|
+
if not ENCRYPTION_AVAILABLE:
|
36
|
+
print(
|
37
|
+
"Error: PyNaCl is required for encryption. Install it with: pip install pynacl"
|
38
|
+
)
|
39
|
+
sys.exit(1)
|
40
|
+
|
41
|
+
# Generate a random key
|
42
|
+
key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
|
43
|
+
|
44
|
+
# Encode to base64 for .env file
|
45
|
+
encoded_key = base64.b64encode(key).decode()
|
46
|
+
|
47
|
+
return encoded_key
|
48
|
+
|
49
|
+
|
50
|
+
def key_generation_cli():
|
51
|
+
"""CLI entry point for encryption key generation."""
|
52
|
+
parser = argparse.ArgumentParser(
|
53
|
+
description="Generate a secure encryption key for Hippius SDK"
|
54
|
+
)
|
55
|
+
parser.add_argument("--copy", action="store_true", help="Copy the key to clipboard")
|
56
|
+
args = parser.parse_args()
|
57
|
+
|
58
|
+
# Generate the key
|
59
|
+
encoded_key = generate_key()
|
60
|
+
|
61
|
+
# Copy to clipboard if requested
|
62
|
+
if args.copy:
|
63
|
+
try:
|
64
|
+
import pyperclip
|
65
|
+
|
66
|
+
pyperclip.copy(encoded_key)
|
67
|
+
print("Key copied to clipboard!")
|
68
|
+
except ImportError:
|
69
|
+
print(
|
70
|
+
"Warning: Could not copy to clipboard. Install pyperclip with: pip install pyperclip"
|
71
|
+
)
|
72
|
+
|
73
|
+
# Print instructions
|
74
|
+
print("\nGenerated a new encryption key for Hippius SDK")
|
75
|
+
print(f"Key: {encoded_key}")
|
76
|
+
print("\nAdd this to your .env file:")
|
77
|
+
print(f"HIPPIUS_ENCRYPTION_KEY={encoded_key}")
|
78
|
+
print("\nOr configure it in your code:")
|
79
|
+
print("import base64")
|
80
|
+
print(f'encryption_key = base64.b64decode("{encoded_key}")')
|
81
|
+
print(
|
82
|
+
"client = HippiusClient(encrypt_by_default=True, encryption_key=encryption_key)"
|
83
|
+
)
|
84
|
+
|
85
|
+
|
86
|
+
def create_client(args):
|
87
|
+
"""Create a HippiusClient instance from command line arguments."""
|
88
|
+
# Process encryption flags
|
89
|
+
encrypt = None
|
90
|
+
if hasattr(args, "encrypt") and args.encrypt:
|
91
|
+
encrypt = True
|
92
|
+
elif hasattr(args, "no_encrypt") and args.no_encrypt:
|
93
|
+
encrypt = False
|
94
|
+
|
95
|
+
decrypt = None
|
96
|
+
if hasattr(args, "decrypt") and args.decrypt:
|
97
|
+
decrypt = True
|
98
|
+
elif hasattr(args, "no_decrypt") and args.no_decrypt:
|
99
|
+
decrypt = False
|
100
|
+
|
101
|
+
# Process encryption key if provided
|
102
|
+
encryption_key = None
|
103
|
+
if hasattr(args, "encryption_key") and args.encryption_key:
|
104
|
+
try:
|
105
|
+
encryption_key = base64.b64decode(args.encryption_key)
|
106
|
+
if args.verbose:
|
107
|
+
print(f"Using provided encryption key")
|
108
|
+
except Exception as e:
|
109
|
+
print(f"Warning: Could not decode encryption key: {e}")
|
110
|
+
print(f"Using default encryption key from .env if available")
|
111
|
+
|
112
|
+
# Initialize client with provided gateway and API URL
|
113
|
+
client = HippiusClient(
|
114
|
+
ipfs_gateway=args.gateway,
|
115
|
+
ipfs_api_url="http://localhost:5001" if args.local_ipfs else args.api_url,
|
116
|
+
substrate_url=args.substrate_url,
|
117
|
+
encrypt_by_default=encrypt,
|
118
|
+
encryption_key=encryption_key,
|
119
|
+
)
|
120
|
+
|
121
|
+
return client, encrypt, decrypt
|
122
|
+
|
123
|
+
|
124
|
+
def handle_download(client, cid, output_path, decrypt=None):
|
125
|
+
"""Handle the download command"""
|
126
|
+
print(f"Downloading {cid} to {output_path}...")
|
127
|
+
|
128
|
+
# Use the enhanced download method which returns formatted information
|
129
|
+
result = client.download_file(cid, output_path, decrypt=decrypt)
|
130
|
+
|
131
|
+
print(f"Download successful in {result['elapsed_seconds']} seconds!")
|
132
|
+
print(f"Saved to: {result['output_path']}")
|
133
|
+
print(f"Size: {result['size_bytes']:,} bytes ({result['size_formatted']})")
|
134
|
+
|
135
|
+
if result.get("decrypted"):
|
136
|
+
print("File was decrypted during download")
|
137
|
+
|
138
|
+
return 0
|
139
|
+
|
140
|
+
|
141
|
+
def handle_exists(client, cid):
|
142
|
+
"""Handle the exists command"""
|
143
|
+
print(f"Checking if CID {cid} exists on IPFS...")
|
144
|
+
result = client.exists(cid)
|
145
|
+
|
146
|
+
# Use the formatted CID from the result
|
147
|
+
formatted_cid = result["formatted_cid"]
|
148
|
+
exists = result["exists"]
|
149
|
+
|
150
|
+
print(f"CID {formatted_cid} exists: {exists}")
|
151
|
+
|
152
|
+
if exists and result.get("gateway_url"):
|
153
|
+
print(f"Gateway URL: {result['gateway_url']}")
|
154
|
+
print("\nTo download this file, you can run:")
|
155
|
+
print(f" hippius download {formatted_cid} <output_path>")
|
156
|
+
|
157
|
+
return 0
|
158
|
+
|
159
|
+
|
160
|
+
def handle_cat(client, cid, max_size, decrypt=None):
|
161
|
+
"""Handle the cat command"""
|
162
|
+
print(f"Retrieving content of CID {cid}...")
|
163
|
+
try:
|
164
|
+
# Use the enhanced cat method with formatting
|
165
|
+
result = client.cat(cid, max_display_bytes=max_size, decrypt=decrypt)
|
166
|
+
|
167
|
+
# Display file information
|
168
|
+
print(
|
169
|
+
f"Content size: {result['size_bytes']:,} bytes ({result['size_formatted']})"
|
170
|
+
)
|
171
|
+
|
172
|
+
if result.get("decrypted"):
|
173
|
+
print("Content was decrypted")
|
174
|
+
|
175
|
+
# Display content based on type
|
176
|
+
if result["is_text"]:
|
177
|
+
print("\nContent (text):")
|
178
|
+
print(result["text_preview"])
|
179
|
+
if result["size_bytes"] > max_size:
|
180
|
+
print(
|
181
|
+
f"\n... (showing first {max_size} bytes of {result['size_bytes']} total) ..."
|
182
|
+
)
|
183
|
+
else:
|
184
|
+
print("\nBinary content (hex):")
|
185
|
+
print(result["hex_preview"])
|
186
|
+
if result["size_bytes"] > max_size:
|
187
|
+
print(
|
188
|
+
f"\n... (showing first {max_size} bytes of {result['size_bytes']} total) ..."
|
189
|
+
)
|
190
|
+
|
191
|
+
except Exception as e:
|
192
|
+
print(f"Error retrieving content: {e}")
|
193
|
+
return 1
|
194
|
+
|
195
|
+
return 0
|
196
|
+
|
197
|
+
|
198
|
+
def handle_store(client, file_path, miner_ids, encrypt=None):
|
199
|
+
"""Handle the store command"""
|
200
|
+
if not os.path.exists(file_path):
|
201
|
+
print(f"Error: File {file_path} not found")
|
202
|
+
return 1
|
203
|
+
|
204
|
+
print(f"Uploading {file_path} to IPFS...")
|
205
|
+
start_time = time.time()
|
206
|
+
|
207
|
+
# Use the enhanced upload_file method that returns formatted information
|
208
|
+
result = client.upload_file(file_path, encrypt=encrypt)
|
209
|
+
|
210
|
+
ipfs_elapsed_time = time.time() - start_time
|
211
|
+
|
212
|
+
print(f"IPFS upload successful in {ipfs_elapsed_time:.2f} seconds!")
|
213
|
+
print(f"CID: {result['cid']}")
|
214
|
+
print(f"Filename: {result['filename']}")
|
215
|
+
print(f"Size: {result['size_bytes']:,} bytes ({result['size_formatted']})")
|
216
|
+
|
217
|
+
if result.get("encrypted"):
|
218
|
+
print("File was encrypted before upload")
|
219
|
+
|
220
|
+
# Store the file on Substrate
|
221
|
+
print("\nStoring the file on Substrate...")
|
222
|
+
start_time = time.time()
|
223
|
+
|
224
|
+
try:
|
225
|
+
# Create a file input object for the marketplace
|
226
|
+
file_input = {"fileHash": result["cid"], "fileName": result["filename"]}
|
227
|
+
|
228
|
+
# Store on Substrate
|
229
|
+
client.substrate_client.storage_request([file_input], miner_ids)
|
230
|
+
|
231
|
+
substrate_elapsed_time = time.time() - start_time
|
232
|
+
print(
|
233
|
+
f"Substrate storage request completed in {substrate_elapsed_time:.2f} seconds!"
|
234
|
+
)
|
235
|
+
|
236
|
+
# Suggestion to verify
|
237
|
+
print("\nTo verify the IPFS upload, you can run:")
|
238
|
+
print(f" hippius exists {result['cid']}")
|
239
|
+
print(f" hippius cat {result['cid']}")
|
240
|
+
|
241
|
+
except NotImplementedError as e:
|
242
|
+
print(f"\nNote: {e}")
|
243
|
+
except Exception as e:
|
244
|
+
print(f"\nError storing file on Substrate: {e}")
|
245
|
+
return 1
|
246
|
+
|
247
|
+
return 0
|
248
|
+
|
249
|
+
|
250
|
+
def handle_store_dir(client, dir_path, miner_ids, encrypt=None):
|
251
|
+
"""Handle the store-dir command"""
|
252
|
+
if not os.path.isdir(dir_path):
|
253
|
+
print(f"Error: Directory {dir_path} not found")
|
254
|
+
return 1
|
255
|
+
|
256
|
+
print(f"Uploading directory {dir_path} to IPFS...")
|
257
|
+
start_time = time.time()
|
258
|
+
|
259
|
+
# We'll manually upload each file first to get individual CIDs
|
260
|
+
all_files = []
|
261
|
+
for root, _, files in os.walk(dir_path):
|
262
|
+
for file in files:
|
263
|
+
file_path = os.path.join(root, file)
|
264
|
+
rel_path = os.path.relpath(file_path, dir_path)
|
265
|
+
all_files.append((file_path, rel_path))
|
266
|
+
|
267
|
+
print(f"Found {len(all_files)} files to upload")
|
268
|
+
|
269
|
+
# Upload each file individually to get all CIDs
|
270
|
+
individual_cids = []
|
271
|
+
for file_path, rel_path in all_files:
|
272
|
+
try:
|
273
|
+
print(f" Uploading: {rel_path}")
|
274
|
+
file_result = client.upload_file(file_path, encrypt=encrypt)
|
275
|
+
individual_cids.append(
|
276
|
+
{
|
277
|
+
"path": rel_path,
|
278
|
+
"cid": file_result["cid"],
|
279
|
+
"filename": file_result["filename"],
|
280
|
+
"size_bytes": file_result["size_bytes"],
|
281
|
+
"size_formatted": file_result.get("size_formatted", ""),
|
282
|
+
"encrypted": file_result.get("encrypted", False),
|
283
|
+
}
|
284
|
+
)
|
285
|
+
print(
|
286
|
+
f" CID: {individual_cids[-1]['cid']} ({individual_cids[-1]['size_formatted']})"
|
287
|
+
)
|
288
|
+
if file_result.get("encrypted"):
|
289
|
+
print(f" Encrypted: Yes")
|
290
|
+
except Exception as e:
|
291
|
+
print(f" Error uploading {rel_path}: {e}")
|
292
|
+
|
293
|
+
# Now upload the entire directory
|
294
|
+
result = client.upload_directory(dir_path, encrypt=encrypt)
|
295
|
+
|
296
|
+
ipfs_elapsed_time = time.time() - start_time
|
297
|
+
|
298
|
+
print(f"\nIPFS directory upload successful in {ipfs_elapsed_time:.2f} seconds!")
|
299
|
+
print(f"Directory CID: {result['cid']}")
|
300
|
+
print(f"Directory name: {result['dirname']}")
|
301
|
+
print(f"Total files: {result.get('file_count', len(individual_cids))}")
|
302
|
+
print(f"Total size: {result.get('size_formatted', 'Unknown')}")
|
303
|
+
|
304
|
+
if result.get("encrypted"):
|
305
|
+
print("Files were encrypted before upload")
|
306
|
+
|
307
|
+
# Print summary of all individual file CIDs
|
308
|
+
print(f"\nAll individual file CIDs ({len(individual_cids)}):")
|
309
|
+
for item in individual_cids:
|
310
|
+
print(f" {item['path']}: {item['cid']} ({item['size_formatted']})")
|
311
|
+
|
312
|
+
# Suggestion to verify
|
313
|
+
print("\nTo verify the IPFS directory upload, you can run:")
|
314
|
+
print(f" hippius exists {result['cid']}")
|
315
|
+
|
316
|
+
# Store all files on Substrate
|
317
|
+
print("\nStoring all files on Substrate...")
|
318
|
+
start_time = time.time()
|
319
|
+
|
320
|
+
try:
|
321
|
+
# Create file input objects for the marketplace
|
322
|
+
file_inputs = []
|
323
|
+
for item in individual_cids:
|
324
|
+
file_inputs.append({"fileHash": item["cid"], "fileName": item["filename"]})
|
325
|
+
|
326
|
+
# Store all files in a single batch request
|
327
|
+
client.substrate_client.storage_request(file_inputs, miner_ids)
|
328
|
+
|
329
|
+
substrate_elapsed_time = time.time() - start_time
|
330
|
+
print(
|
331
|
+
f"Substrate storage request completed in {substrate_elapsed_time:.2f} seconds!"
|
332
|
+
)
|
333
|
+
|
334
|
+
except NotImplementedError as e:
|
335
|
+
print(f"\nNote: {e}")
|
336
|
+
except Exception as e:
|
337
|
+
print(f"\nError storing files on Substrate: {e}")
|
338
|
+
return 1
|
339
|
+
|
340
|
+
return 0
|
341
|
+
|
342
|
+
|
343
|
+
def handle_credits(client, account_address):
|
344
|
+
"""Handle the credits command"""
|
345
|
+
print("Checking free credits for the account...")
|
346
|
+
try:
|
347
|
+
credits = client.substrate_client.get_free_credits(account_address)
|
348
|
+
print(f"\nFree credits: {credits:.6f}")
|
349
|
+
raw_value = int(
|
350
|
+
credits * 1_000_000_000_000_000_000
|
351
|
+
) # Convert back to raw for display
|
352
|
+
print(f"Raw value: {raw_value:,}")
|
353
|
+
print(
|
354
|
+
f"Account address: {account_address or client.substrate_client._keypair.ss58_address}"
|
355
|
+
)
|
356
|
+
except Exception as e:
|
357
|
+
print(f"Error checking credits: {e}")
|
358
|
+
return 1
|
359
|
+
|
360
|
+
return 0
|
361
|
+
|
362
|
+
|
363
|
+
def handle_files(client, account_address, debug=False, show_all_miners=False):
|
364
|
+
"""Handle the files command"""
|
365
|
+
print("Retrieving file information...")
|
366
|
+
try:
|
367
|
+
if debug:
|
368
|
+
print("DEBUG MODE: Will show details about CID decoding")
|
369
|
+
|
370
|
+
# Use the enhanced get_user_files method with our preferences
|
371
|
+
max_miners = 0 if show_all_miners else 3 # 0 means show all miners
|
372
|
+
files = client.substrate_client.get_user_files(
|
373
|
+
account_address,
|
374
|
+
truncate_miners=True, # Always truncate long miner IDs
|
375
|
+
max_miners=max_miners, # Use 0 for all or 3 for limited
|
376
|
+
)
|
377
|
+
|
378
|
+
if files:
|
379
|
+
print(
|
380
|
+
f"\nFound {len(files)} files for account: {account_address or client.substrate_client._keypair.ss58_address}"
|
381
|
+
)
|
382
|
+
print("\n" + "-" * 80)
|
383
|
+
|
384
|
+
for i, file in enumerate(files, 1):
|
385
|
+
print(f"File {i}:")
|
386
|
+
|
387
|
+
# Format the CID using the SDK method
|
388
|
+
file_hash = file.get("file_hash", "Unknown")
|
389
|
+
formatted_cid = client.format_cid(file_hash)
|
390
|
+
print(f" File Hash (CID): {formatted_cid}")
|
391
|
+
|
392
|
+
# Display file name
|
393
|
+
print(f" File Name: {file.get('file_name', 'Unnamed')}")
|
394
|
+
|
395
|
+
# Display file size with SDK formatting method if needed
|
396
|
+
file_size = file.get("file_size", 0)
|
397
|
+
size_formatted = file.get("size_formatted")
|
398
|
+
if not size_formatted and file_size > 0:
|
399
|
+
size_formatted = client.format_size(file_size)
|
400
|
+
print(f" File Size: {file_size:,} bytes ({size_formatted})")
|
401
|
+
|
402
|
+
# Display miners
|
403
|
+
miner_count = file.get("miner_count", 0)
|
404
|
+
miners = file.get("miner_ids", [])
|
405
|
+
|
406
|
+
if miner_count > 0:
|
407
|
+
print(f" Pinned by {miner_count} miners:")
|
408
|
+
|
409
|
+
# Show message about truncated list if applicable
|
410
|
+
if miner_count > len(miners) and not show_all_miners:
|
411
|
+
print(
|
412
|
+
f" (Showing {len(miners)} of {miner_count} miners - use --all-miners to see all)"
|
413
|
+
)
|
414
|
+
elif miner_count > 3 and show_all_miners:
|
415
|
+
print(f" (Showing all {miner_count} miners)")
|
416
|
+
|
417
|
+
# Display the miners using their formatted IDs
|
418
|
+
for miner in miners:
|
419
|
+
if isinstance(miner, dict) and "formatted" in miner:
|
420
|
+
print(f" - {miner['formatted']}")
|
421
|
+
else:
|
422
|
+
print(f" - {miner}")
|
423
|
+
else:
|
424
|
+
print(" Not pinned by any miners")
|
425
|
+
|
426
|
+
print("-" * 80)
|
427
|
+
else:
|
428
|
+
print(
|
429
|
+
f"No files found for account: {account_address or client.substrate_client._keypair.ss58_address}"
|
430
|
+
)
|
431
|
+
except Exception as e:
|
432
|
+
print(f"Error retrieving file information: {e}")
|
433
|
+
return 1
|
434
|
+
|
435
|
+
return 0
|
436
|
+
|
437
|
+
|
438
|
+
def main():
|
439
|
+
"""Main entry point for the Hippius CLI."""
|
440
|
+
parser = argparse.ArgumentParser(
|
441
|
+
description="Hippius SDK Command Line Interface",
|
442
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
443
|
+
epilog="""
|
444
|
+
Examples:
|
445
|
+
hippius download QmCID123 downloaded_file.txt
|
446
|
+
hippius exists QmCID123
|
447
|
+
hippius cat QmCID123
|
448
|
+
hippius store test_file.txt
|
449
|
+
hippius store-dir ./test_directory
|
450
|
+
hippius credits
|
451
|
+
hippius credits 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
452
|
+
hippius files
|
453
|
+
hippius files 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
454
|
+
hippius files --all-miners
|
455
|
+
hippius keygen
|
456
|
+
hippius keygen --copy
|
457
|
+
""",
|
458
|
+
)
|
459
|
+
|
460
|
+
# Optional arguments for all commands
|
461
|
+
parser.add_argument(
|
462
|
+
"--gateway",
|
463
|
+
default=os.getenv("IPFS_GATEWAY", "https://ipfs.io"),
|
464
|
+
help="IPFS gateway URL for downloads (default: from env or https://ipfs.io)",
|
465
|
+
)
|
466
|
+
parser.add_argument(
|
467
|
+
"--api-url",
|
468
|
+
default=os.getenv("IPFS_API_URL", "https://relay-fr.hippius.network"),
|
469
|
+
help="IPFS API URL for uploads (default: from env or https://relay-fr.hippius.network)",
|
470
|
+
)
|
471
|
+
parser.add_argument(
|
472
|
+
"--local-ipfs",
|
473
|
+
action="store_true",
|
474
|
+
help="Use local IPFS node (http://localhost:5001) instead of remote API",
|
475
|
+
)
|
476
|
+
parser.add_argument(
|
477
|
+
"--substrate-url",
|
478
|
+
default=os.getenv("SUBSTRATE_URL", "wss://rpc.hippius.network"),
|
479
|
+
help="Substrate node WebSocket URL (default: from env or wss://rpc.hippius.network)",
|
480
|
+
)
|
481
|
+
parser.add_argument(
|
482
|
+
"--miner-ids",
|
483
|
+
help="Comma-separated list of miner IDs for storage (default: from env SUBSTRATE_DEFAULT_MINERS)",
|
484
|
+
)
|
485
|
+
parser.add_argument(
|
486
|
+
"--verbose", "-v", action="store_true", help="Enable verbose debug output"
|
487
|
+
)
|
488
|
+
parser.add_argument(
|
489
|
+
"--encrypt",
|
490
|
+
action="store_true",
|
491
|
+
help="Encrypt files when uploading (overrides default)",
|
492
|
+
)
|
493
|
+
parser.add_argument(
|
494
|
+
"--no-encrypt",
|
495
|
+
action="store_true",
|
496
|
+
help="Do not encrypt files when uploading (overrides default)",
|
497
|
+
)
|
498
|
+
parser.add_argument(
|
499
|
+
"--decrypt",
|
500
|
+
action="store_true",
|
501
|
+
help="Decrypt files when downloading (overrides default)",
|
502
|
+
)
|
503
|
+
parser.add_argument(
|
504
|
+
"--no-decrypt",
|
505
|
+
action="store_true",
|
506
|
+
help="Do not decrypt files when downloading (overrides default)",
|
507
|
+
)
|
508
|
+
parser.add_argument(
|
509
|
+
"--encryption-key",
|
510
|
+
help="Base64-encoded encryption key (overrides HIPPIUS_ENCRYPTION_KEY in .env)",
|
511
|
+
)
|
512
|
+
|
513
|
+
# Subcommands
|
514
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
515
|
+
|
516
|
+
# Download command
|
517
|
+
download_parser = subparsers.add_parser(
|
518
|
+
"download", help="Download a file from IPFS"
|
519
|
+
)
|
520
|
+
download_parser.add_argument("cid", help="CID of file to download")
|
521
|
+
download_parser.add_argument("output_path", help="Path to save downloaded file")
|
522
|
+
|
523
|
+
# Exists command
|
524
|
+
exists_parser = subparsers.add_parser(
|
525
|
+
"exists", help="Check if a CID exists on IPFS"
|
526
|
+
)
|
527
|
+
exists_parser.add_argument("cid", help="CID to check")
|
528
|
+
|
529
|
+
# Cat command
|
530
|
+
cat_parser = subparsers.add_parser(
|
531
|
+
"cat", help="Display content of a file from IPFS"
|
532
|
+
)
|
533
|
+
cat_parser.add_argument("cid", help="CID of file to display")
|
534
|
+
cat_parser.add_argument(
|
535
|
+
"--max-size",
|
536
|
+
type=int,
|
537
|
+
default=1024,
|
538
|
+
help="Maximum number of bytes to display (default: 1024)",
|
539
|
+
)
|
540
|
+
|
541
|
+
# Store command (upload to IPFS then store on Substrate)
|
542
|
+
store_parser = subparsers.add_parser(
|
543
|
+
"store", help="Upload a file to IPFS and store it on Substrate"
|
544
|
+
)
|
545
|
+
store_parser.add_argument("file_path", help="Path to file to upload")
|
546
|
+
|
547
|
+
# Store directory command
|
548
|
+
store_dir_parser = subparsers.add_parser(
|
549
|
+
"store-dir", help="Upload a directory to IPFS and store all files on Substrate"
|
550
|
+
)
|
551
|
+
store_dir_parser.add_argument("dir_path", help="Path to directory to upload")
|
552
|
+
|
553
|
+
# Credits command
|
554
|
+
credits_parser = subparsers.add_parser(
|
555
|
+
"credits", help="Check free credits for an account in the marketplace"
|
556
|
+
)
|
557
|
+
credits_parser.add_argument(
|
558
|
+
"account_address",
|
559
|
+
nargs="?",
|
560
|
+
default=None,
|
561
|
+
help="Substrate account address (uses keypair address if not specified)",
|
562
|
+
)
|
563
|
+
|
564
|
+
# Files command
|
565
|
+
files_parser = subparsers.add_parser(
|
566
|
+
"files", help="View detailed information about files stored by a user"
|
567
|
+
)
|
568
|
+
files_parser.add_argument(
|
569
|
+
"account_address",
|
570
|
+
nargs="?",
|
571
|
+
default=None,
|
572
|
+
help="Substrate account address (uses keypair address if not specified)",
|
573
|
+
)
|
574
|
+
files_parser.add_argument(
|
575
|
+
"--debug", action="store_true", help="Show debug information about CID decoding"
|
576
|
+
)
|
577
|
+
files_parser.add_argument(
|
578
|
+
"--all-miners",
|
579
|
+
action="store_true",
|
580
|
+
help="Show all miners for each file instead of only the first 3",
|
581
|
+
)
|
582
|
+
|
583
|
+
# Key generation command
|
584
|
+
keygen_parser = subparsers.add_parser(
|
585
|
+
"keygen", help="Generate an encryption key for secure file storage"
|
586
|
+
)
|
587
|
+
keygen_parser.add_argument(
|
588
|
+
"--copy", action="store_true", help="Copy the generated key to the clipboard"
|
589
|
+
)
|
590
|
+
|
591
|
+
args = parser.parse_args()
|
592
|
+
|
593
|
+
if not args.command:
|
594
|
+
parser.print_help()
|
595
|
+
return 1
|
596
|
+
|
597
|
+
# Special case for keygen which doesn't need client initialization
|
598
|
+
if args.command == "keygen":
|
599
|
+
# Handle key generation separately
|
600
|
+
if args.copy:
|
601
|
+
return key_generation_cli()
|
602
|
+
else:
|
603
|
+
# Create a new argparse namespace with just the copy flag for compatibility
|
604
|
+
keygen_args = argparse.Namespace(copy=False)
|
605
|
+
return key_generation_cli()
|
606
|
+
|
607
|
+
try:
|
608
|
+
# Parse miner IDs if provided
|
609
|
+
miner_ids = None
|
610
|
+
if args.miner_ids:
|
611
|
+
miner_ids = [miner.strip() for miner in args.miner_ids.split(",")]
|
612
|
+
elif os.getenv("SUBSTRATE_DEFAULT_MINERS"):
|
613
|
+
miner_ids = [
|
614
|
+
miner.strip()
|
615
|
+
for miner in os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
|
616
|
+
]
|
617
|
+
|
618
|
+
# Create client
|
619
|
+
client, encrypt, decrypt = create_client(args)
|
620
|
+
|
621
|
+
# Handle commands
|
622
|
+
if args.command == "download":
|
623
|
+
return handle_download(client, args.cid, args.output_path, decrypt=decrypt)
|
624
|
+
|
625
|
+
elif args.command == "exists":
|
626
|
+
return handle_exists(client, args.cid)
|
627
|
+
|
628
|
+
elif args.command == "cat":
|
629
|
+
return handle_cat(client, args.cid, args.max_size, decrypt=decrypt)
|
630
|
+
|
631
|
+
elif args.command == "store":
|
632
|
+
return handle_store(client, args.file_path, miner_ids, encrypt=encrypt)
|
633
|
+
|
634
|
+
elif args.command == "store-dir":
|
635
|
+
return handle_store_dir(client, args.dir_path, miner_ids, encrypt=encrypt)
|
636
|
+
|
637
|
+
elif args.command == "credits":
|
638
|
+
return handle_credits(client, args.account_address)
|
639
|
+
|
640
|
+
elif args.command == "files":
|
641
|
+
return handle_files(
|
642
|
+
client,
|
643
|
+
args.account_address,
|
644
|
+
debug=args.debug if hasattr(args, "debug") else False,
|
645
|
+
show_all_miners=args.all_miners
|
646
|
+
if hasattr(args, "all_miners")
|
647
|
+
else False,
|
648
|
+
)
|
649
|
+
|
650
|
+
except Exception as e:
|
651
|
+
print(f"Error: {e}")
|
652
|
+
return 1
|
653
|
+
|
654
|
+
return 0
|
655
|
+
|
656
|
+
|
657
|
+
if __name__ == "__main__":
|
658
|
+
sys.exit(main())
|