hippius 0.2.39__py3-none-any.whl → 0.2.41__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.2.39.dist-info → hippius-0.2.41.dist-info}/METADATA +1 -1
- {hippius-0.2.39.dist-info → hippius-0.2.41.dist-info}/RECORD +8 -8
- hippius_sdk/__init__.py +1 -1
- hippius_sdk/client.py +47 -30
- hippius_sdk/ipfs.py +174 -85
- hippius_sdk/ipfs_core.py +9 -4
- {hippius-0.2.39.dist-info → hippius-0.2.41.dist-info}/WHEEL +0 -0
- {hippius-0.2.39.dist-info → hippius-0.2.41.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,10 @@
|
|
1
|
-
hippius_sdk/__init__.py,sha256=
|
1
|
+
hippius_sdk/__init__.py,sha256=0Xf8Fnc93-TuSh6wLqELTb2OV2h1tfOmTnMDfe4i2RM,1474
|
2
2
|
hippius_sdk/cli.py,sha256=aqKOYSBSWt7UhcpFt7wf9yIPJ3bznpsJ6ehOnuZ4usI,18235
|
3
3
|
hippius_sdk/cli_assets.py,sha256=rjH3Z5A1CQr2d5CIAAAb0WMCjoZZlMWcdo0f93KqluE,635
|
4
4
|
hippius_sdk/cli_handlers.py,sha256=HkZldE8ZDS6WHu8aSoeS_rYZ4kp3F-Kdzu-weY1c0vU,128258
|
5
5
|
hippius_sdk/cli_parser.py,sha256=z7UvgWvvy04ey-R56qZiCqYc_9RaNq1rVDkQyXoK3JU,21100
|
6
6
|
hippius_sdk/cli_rich.py,sha256=_jTBYMdHi2--fIVwoeNi-EtkdOb6Zy_O2TUiGvU3O7s,7324
|
7
|
-
hippius_sdk/client.py,sha256=
|
7
|
+
hippius_sdk/client.py,sha256=n7QwLneWZym_eKN1fEAJWEi98nSJjdSpZJeEcFjZsqQ,24197
|
8
8
|
hippius_sdk/config.py,sha256=Hf_aUYzG9ylzqauA_ABUSSB5mBTYbp-VtB36VQt2XDw,21981
|
9
9
|
hippius_sdk/db/README.md,sha256=okDeI1qgkaZqXSlJ8L0xIE4UpuxO-qEGPIbXUvSHQjU,2030
|
10
10
|
hippius_sdk/db/env.db.template,sha256=_6hEC3IvkzCDOAzG1_yJUKRUfCTMciNaJUicZpMCat4,217
|
@@ -13,12 +13,12 @@ hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql,sha
|
|
13
13
|
hippius_sdk/db/setup_database.sh,sha256=STp03qxkp2RmIVr6YZIcvQQm-_LLUOb6Jobh-52HWmg,3115
|
14
14
|
hippius_sdk/db_utils.py,sha256=-x0rbN0as7Tn3PJPZBYCgreZe52FLH40ppA1TLxsg90,1851
|
15
15
|
hippius_sdk/errors.py,sha256=LScJJmawVAx7aRzqqQguYSkf9iazSjEQEBNlD_GXZ6Y,1589
|
16
|
-
hippius_sdk/ipfs.py,sha256=
|
17
|
-
hippius_sdk/ipfs_core.py,sha256=
|
16
|
+
hippius_sdk/ipfs.py,sha256=uBqjXzF8chKoFcyx9Jmuw9XCINf9qJ3Os1UBPdBsucc,100667
|
17
|
+
hippius_sdk/ipfs_core.py,sha256=WG7bGLk-threOvmumizwh1dnd5zqbIkTXy1y-BRGayI,12789
|
18
18
|
hippius_sdk/key_storage.py,sha256=SXFd6aGQw9MDLGX2vSBuAY7rdX-k5EvFm63z7_n-8yQ,8148
|
19
19
|
hippius_sdk/substrate.py,sha256=4a7UIE4UqGcDW7luKTBgSDqfb2OIZusB39G1UiRs_YU,50158
|
20
20
|
hippius_sdk/utils.py,sha256=rJ611yvwKSyiBpYU3w-SuyQxoghMGU-ePuslrPv5H5g,7388
|
21
|
-
hippius-0.2.
|
22
|
-
hippius-0.2.
|
23
|
-
hippius-0.2.
|
24
|
-
hippius-0.2.
|
21
|
+
hippius-0.2.41.dist-info/METADATA,sha256=SSSssIaVQf94BZRTCMotY-zQGwUuByoPwA6svR7ufQE,30088
|
22
|
+
hippius-0.2.41.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
23
|
+
hippius-0.2.41.dist-info/entry_points.txt,sha256=bFAZjW3vndretf9-8s587jA2ebMVI7puhn_lVs8jPc8,149
|
24
|
+
hippius-0.2.41.dist-info/RECORD,,
|
hippius_sdk/__init__.py
CHANGED
@@ -26,7 +26,7 @@ from hippius_sdk.config import (
|
|
26
26
|
from hippius_sdk.ipfs import IPFSClient, S3PublishResult, S3DownloadResult
|
27
27
|
from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
|
28
28
|
|
29
|
-
__version__ = "0.2.
|
29
|
+
__version__ = "0.2.41"
|
30
30
|
__all__ = [
|
31
31
|
"HippiusClient",
|
32
32
|
"IPFSClient",
|
hippius_sdk/client.py
CHANGED
@@ -3,7 +3,8 @@ Main client for the Hippius SDK.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import base64
|
6
|
-
|
6
|
+
import os
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
|
7
8
|
|
8
9
|
import nacl.secret
|
9
10
|
import nacl.utils
|
@@ -118,7 +119,9 @@ class HippiusClient:
|
|
118
119
|
"""
|
119
120
|
# Use the enhanced IPFSClient method directly with encryption parameter
|
120
121
|
return await self.ipfs_client.upload_file(
|
121
|
-
file_path,
|
122
|
+
file_path,
|
123
|
+
encrypt=encrypt,
|
124
|
+
seed_phrase=seed_phrase,
|
122
125
|
)
|
123
126
|
|
124
127
|
async def upload_directory(
|
@@ -517,25 +520,27 @@ class HippiusClient:
|
|
517
520
|
|
518
521
|
async def s3_publish(
|
519
522
|
self,
|
520
|
-
|
523
|
+
content: Union[str, bytes, os.PathLike],
|
521
524
|
encrypt: bool,
|
522
525
|
seed_phrase: str,
|
523
526
|
subaccount_id: str,
|
524
527
|
bucket_name: str,
|
528
|
+
file_name: str = None,
|
525
529
|
store_node: str = "http://localhost:5001",
|
526
530
|
pin_node: str = "https://store.hippius.network",
|
527
531
|
substrate_url: str = "wss://rpc.hippius.network",
|
528
532
|
publish: bool = True,
|
529
533
|
) -> Union[S3PublishResult, S3PublishPin]:
|
530
534
|
"""
|
531
|
-
Publish
|
535
|
+
Publish content to IPFS and the Hippius marketplace in one operation.
|
532
536
|
|
533
537
|
Uses a two-node architecture for optimal performance:
|
534
538
|
1. Uploads to store_node (local) for immediate availability
|
535
539
|
2. Pins to pin_node (remote) for persistence and backup
|
536
540
|
|
537
541
|
Args:
|
538
|
-
|
542
|
+
content: Either a file path (str/PathLike) or bytes content to publish
|
543
|
+
file_name: The original file name (required if content is bytes)
|
539
544
|
encrypt: Whether to encrypt the file before uploading
|
540
545
|
seed_phrase: Seed phrase for blockchain transaction signing
|
541
546
|
subaccount_id: The subaccount/account identifier
|
@@ -556,46 +561,51 @@ class HippiusClient:
|
|
556
561
|
ValueError: If encryption is requested but not available
|
557
562
|
"""
|
558
563
|
return await self.ipfs_client.s3_publish(
|
559
|
-
|
560
|
-
encrypt,
|
561
|
-
seed_phrase,
|
562
|
-
subaccount_id,
|
563
|
-
bucket_name,
|
564
|
-
store_node,
|
565
|
-
pin_node,
|
566
|
-
substrate_url,
|
567
|
-
publish,
|
564
|
+
content=content,
|
565
|
+
encrypt=encrypt,
|
566
|
+
seed_phrase=seed_phrase,
|
567
|
+
subaccount_id=subaccount_id,
|
568
|
+
bucket_name=bucket_name,
|
569
|
+
store_node=store_node,
|
570
|
+
pin_node=pin_node,
|
571
|
+
substrate_url=substrate_url,
|
572
|
+
publish=publish,
|
573
|
+
file_name=file_name,
|
568
574
|
)
|
569
575
|
|
570
576
|
async def s3_download(
|
571
577
|
self,
|
572
578
|
cid: str,
|
573
|
-
output_path: str,
|
574
|
-
subaccount_id: str,
|
575
|
-
bucket_name: str,
|
579
|
+
output_path: Optional[str] = None,
|
580
|
+
subaccount_id: Optional[str] = None,
|
581
|
+
bucket_name: Optional[str] = None,
|
576
582
|
auto_decrypt: bool = True,
|
577
583
|
download_node: str = "http://localhost:5001",
|
578
|
-
|
584
|
+
return_bytes: bool = False,
|
585
|
+
streaming: bool = False,
|
586
|
+
) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
|
579
587
|
"""
|
580
|
-
Download
|
588
|
+
Download content from IPFS with flexible output options and automatic decryption.
|
581
589
|
|
582
|
-
This method
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
- Falls back to client encryption key if key storage is not available
|
587
|
-
- Returns the file in decrypted form if decryption succeeds
|
590
|
+
This method provides multiple output modes:
|
591
|
+
1. File output: Downloads to specified path (default mode)
|
592
|
+
2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
|
593
|
+
3. Streaming output: Returns raw streaming iterator from IPFS node (streaming=True)
|
588
594
|
|
589
595
|
Args:
|
590
596
|
cid: Content Identifier (CID) of the file to download
|
591
|
-
output_path: Path where the downloaded file will be saved
|
592
|
-
subaccount_id: The subaccount/account identifier
|
593
|
-
bucket_name: The bucket name for key isolation
|
597
|
+
output_path: Path where the downloaded file will be saved (None for bytes/streaming)
|
598
|
+
subaccount_id: The subaccount/account identifier (required for decryption)
|
599
|
+
bucket_name: The bucket name for key isolation (required for decryption)
|
594
600
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
595
601
|
download_node: IPFS node URL for download (default: local node)
|
602
|
+
return_bytes: If True, return bytes instead of saving to file
|
603
|
+
streaming: If True, return raw streaming iterator from IPFS (no decryption)
|
596
604
|
|
597
605
|
Returns:
|
598
|
-
S3DownloadResult:
|
606
|
+
S3DownloadResult: Download info and decryption status (default)
|
607
|
+
bytes: Raw decrypted content when return_bytes=True
|
608
|
+
AsyncIterator[bytes]: Raw streaming iterator when streaming=True
|
599
609
|
|
600
610
|
Raises:
|
601
611
|
HippiusIPFSError: If IPFS download fails
|
@@ -603,5 +613,12 @@ class HippiusClient:
|
|
603
613
|
ValueError: If decryption fails
|
604
614
|
"""
|
605
615
|
return await self.ipfs_client.s3_download(
|
606
|
-
cid,
|
616
|
+
cid=cid,
|
617
|
+
output_path=output_path,
|
618
|
+
subaccount_id=subaccount_id,
|
619
|
+
bucket_name=bucket_name,
|
620
|
+
auto_decrypt=auto_decrypt,
|
621
|
+
download_node=download_node,
|
622
|
+
return_bytes=return_bytes,
|
623
|
+
streaming=streaming,
|
607
624
|
)
|
hippius_sdk/ipfs.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
IPFS operations for the Hippius SDK.
|
3
3
|
"""
|
4
|
+
|
4
5
|
import asyncio
|
5
6
|
import base64
|
6
7
|
import hashlib
|
@@ -12,7 +13,7 @@ import shutil
|
|
12
13
|
import tempfile
|
13
14
|
import time
|
14
15
|
import uuid
|
15
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
|
16
17
|
|
17
18
|
import httpx
|
18
19
|
from pydantic import BaseModel
|
@@ -129,7 +130,7 @@ class IPFSClient:
|
|
129
130
|
|
130
131
|
try:
|
131
132
|
self.client = AsyncIPFSClient(api_url=api_url, gateway=self.gateway)
|
132
|
-
except httpx.ConnectError
|
133
|
+
except httpx.ConnectError:
|
133
134
|
print(
|
134
135
|
f"Warning: Falling back to local IPFS daemon, but still using gateway={self.gateway}"
|
135
136
|
)
|
@@ -241,6 +242,7 @@ class IPFSClient:
|
|
241
242
|
encrypt: Optional[bool] = None,
|
242
243
|
max_retries: int = 3,
|
243
244
|
seed_phrase: Optional[str] = None,
|
245
|
+
file_name: str = None,
|
244
246
|
) -> Dict[str, Any]:
|
245
247
|
"""
|
246
248
|
Upload a file to IPFS with optional encryption.
|
@@ -251,6 +253,7 @@ class IPFSClient:
|
|
251
253
|
encrypt: Whether to encrypt the file (overrides default)
|
252
254
|
max_retries: Maximum number of retry attempts (default: 3)
|
253
255
|
seed_phrase: Optional seed phrase to use for blockchain interactions (uses config if None)
|
256
|
+
file_name: The original file name
|
254
257
|
|
255
258
|
Returns:
|
256
259
|
Dict[str, Any]: Dictionary containing:
|
@@ -278,7 +281,7 @@ class IPFSClient:
|
|
278
281
|
)
|
279
282
|
|
280
283
|
# Get file info before upload
|
281
|
-
|
284
|
+
file_name = file_name if file_name else os.path.basename(file_path)
|
282
285
|
size_bytes = os.path.getsize(file_path)
|
283
286
|
|
284
287
|
# If encryption is requested, encrypt the file first
|
@@ -303,7 +306,10 @@ class IPFSClient:
|
|
303
306
|
# Use the original file for upload
|
304
307
|
upload_path = file_path
|
305
308
|
|
306
|
-
result = await self.client.add_file(
|
309
|
+
result = await self.client.add_file(
|
310
|
+
upload_path,
|
311
|
+
file_name=file_name,
|
312
|
+
)
|
307
313
|
cid = result["Hash"]
|
308
314
|
|
309
315
|
finally:
|
@@ -314,7 +320,7 @@ class IPFSClient:
|
|
314
320
|
# Format the result
|
315
321
|
result = {
|
316
322
|
"cid": cid,
|
317
|
-
"filename":
|
323
|
+
"filename": file_name,
|
318
324
|
"size_bytes": size_bytes,
|
319
325
|
"encrypted": should_encrypt,
|
320
326
|
}
|
@@ -1411,7 +1417,7 @@ class IPFSClient:
|
|
1411
1417
|
|
1412
1418
|
for i, chunk in enumerate(reconstructed_chunks):
|
1413
1419
|
if verbose and i % 10 == 0:
|
1414
|
-
print(f"Processing chunk {i+1}/{len(reconstructed_chunks)}...")
|
1420
|
+
print(f"Processing chunk {i + 1}/{len(reconstructed_chunks)}...")
|
1415
1421
|
|
1416
1422
|
# For all chunks except the last one, use full chunk size
|
1417
1423
|
if i < len(reconstructed_chunks) - 1:
|
@@ -1750,8 +1756,9 @@ class IPFSClient:
|
|
1750
1756
|
# Record the child files that were processed
|
1751
1757
|
result["child_files"] = child_files
|
1752
1758
|
except Exception as e:
|
1753
|
-
print(
|
1754
|
-
|
1759
|
+
print(
|
1760
|
+
f"Warning: Failed to check if CID is a directory: {e}"
|
1761
|
+
) # Continue with regular file unpin
|
1755
1762
|
|
1756
1763
|
# Now unpin the main file/directory
|
1757
1764
|
if unpin:
|
@@ -1821,7 +1828,8 @@ class IPFSClient:
|
|
1821
1828
|
cancel_from_blockchain: bool = True,
|
1822
1829
|
parallel_limit: int = 20,
|
1823
1830
|
seed_phrase: Optional[str] = None,
|
1824
|
-
metadata_timeout: int = 30,
|
1831
|
+
metadata_timeout: int = 30,
|
1832
|
+
# Timeout in seconds for metadata fetch
|
1825
1833
|
) -> bool:
|
1826
1834
|
"""
|
1827
1835
|
Delete an erasure-coded file, including all its chunks in parallel.
|
@@ -1863,8 +1871,7 @@ class IPFSClient:
|
|
1863
1871
|
except asyncio.TimeoutError:
|
1864
1872
|
print(
|
1865
1873
|
f"Timed out after {metadata_timeout}s waiting for metadata download"
|
1866
|
-
)
|
1867
|
-
# We'll continue with blockchain cancellation even without metadata
|
1874
|
+
) # We'll continue with blockchain cancellation even without metadata
|
1868
1875
|
except json.JSONDecodeError as e:
|
1869
1876
|
# If we can't parse the metadata JSON, record the error but continue
|
1870
1877
|
print(f"Error parsing metadata JSON: {e}")
|
@@ -1960,7 +1967,7 @@ class IPFSClient:
|
|
1960
1967
|
|
1961
1968
|
async def s3_publish(
|
1962
1969
|
self,
|
1963
|
-
|
1970
|
+
content: Union[str, bytes, os.PathLike],
|
1964
1971
|
encrypt: bool,
|
1965
1972
|
seed_phrase: str,
|
1966
1973
|
subaccount_id: str,
|
@@ -1969,9 +1976,10 @@ class IPFSClient:
|
|
1969
1976
|
pin_node: str = "https://store.hippius.network",
|
1970
1977
|
substrate_url: str = "wss://rpc.hippius.network",
|
1971
1978
|
publish: bool = True,
|
1979
|
+
file_name: str = None,
|
1972
1980
|
) -> Union[S3PublishResult, S3PublishPin]:
|
1973
1981
|
"""
|
1974
|
-
Publish
|
1982
|
+
Publish content to IPFS and the Hippius marketplace in one operation.
|
1975
1983
|
|
1976
1984
|
This method uses a two-node architecture for optimal performance:
|
1977
1985
|
1. Uploads to store_node (local) for immediate availability
|
@@ -1984,8 +1992,8 @@ class IPFSClient:
|
|
1984
1992
|
- Always uses the most recent key for an account+bucket combination
|
1985
1993
|
|
1986
1994
|
Args:
|
1987
|
-
|
1988
|
-
encrypt: Whether to encrypt the
|
1995
|
+
content: Either a file path (str/PathLike) or bytes content to publish
|
1996
|
+
encrypt: Whether to encrypt the content before uploading
|
1989
1997
|
seed_phrase: Seed phrase for blockchain transaction signing
|
1990
1998
|
subaccount_id: The subaccount/account identifier
|
1991
1999
|
bucket_name: The bucket name for key isolation
|
@@ -1993,24 +2001,47 @@ class IPFSClient:
|
|
1993
2001
|
pin_node: IPFS node URL for backup pinning (default: remote service)
|
1994
2002
|
substrate_url: the substrate url to connect to for the storage request
|
1995
2003
|
publish: Whether to publish to blockchain (True) or just upload to IPFS (False)
|
2004
|
+
file_name: The original file name (required if content is bytes)
|
1996
2005
|
|
1997
2006
|
Returns:
|
1998
2007
|
S3PublishResult: Object containing CID, file info, and transaction hash when publish=True
|
1999
|
-
S3PublishPin: Object containing CID, subaccount,
|
2008
|
+
S3PublishPin: Object containing CID, subaccount, content info, pin_node, substrate_url when publish=False
|
2000
2009
|
|
2001
2010
|
Raises:
|
2002
2011
|
HippiusIPFSError: If IPFS operations (add or pin) fail
|
2003
2012
|
HippiusSubstrateError: If substrate call fails
|
2004
|
-
FileNotFoundError: If the file doesn't exist
|
2005
|
-
ValueError: If encryption is requested but not available
|
2013
|
+
FileNotFoundError: If the file doesn't exist (when content is a path)
|
2014
|
+
ValueError: If encryption is requested but not available, or if file_name is missing for bytes content
|
2006
2015
|
"""
|
2007
|
-
#
|
2008
|
-
|
2009
|
-
|
2016
|
+
# Determine if content is a file path or bytes
|
2017
|
+
is_file_path = isinstance(content, (str, os.PathLike))
|
2018
|
+
|
2019
|
+
if is_file_path:
|
2020
|
+
# Handle file path input
|
2021
|
+
file_path = str(content)
|
2022
|
+
if not os.path.exists(file_path):
|
2023
|
+
raise FileNotFoundError(f"File {file_path} not found")
|
2024
|
+
|
2025
|
+
# Get file info
|
2026
|
+
filename = file_name or os.path.basename(file_path)
|
2027
|
+
size_bytes = os.path.getsize(file_path)
|
2028
|
+
|
2029
|
+
# Read file content into memory
|
2030
|
+
with open(file_path, "rb") as f:
|
2031
|
+
content_bytes = f.read()
|
2032
|
+
else:
|
2033
|
+
# Handle bytes input
|
2034
|
+
if not isinstance(content, bytes):
|
2035
|
+
raise ValueError(
|
2036
|
+
f"Content must be str, PathLike, or bytes, got {type(content)}"
|
2037
|
+
)
|
2010
2038
|
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2039
|
+
if not file_name:
|
2040
|
+
raise ValueError("file_name is required when content is bytes")
|
2041
|
+
|
2042
|
+
filename = file_name
|
2043
|
+
content_bytes = content
|
2044
|
+
size_bytes = len(content_bytes)
|
2014
2045
|
|
2015
2046
|
# Handle encryption if requested with automatic key management
|
2016
2047
|
encryption_key_used = None
|
@@ -2048,19 +2079,11 @@ class IPFSClient:
|
|
2048
2079
|
encryption_key_bytes = base64.b64decode(new_key_b64)
|
2049
2080
|
encryption_key_used = new_key_b64
|
2050
2081
|
|
2051
|
-
# Read file content into memory
|
2052
|
-
with open(file_path, "rb") as f:
|
2053
|
-
file_data = f.read()
|
2054
|
-
|
2055
2082
|
# Encrypt the data using the key from key storage
|
2056
2083
|
import nacl.secret
|
2057
2084
|
|
2058
2085
|
box = nacl.secret.SecretBox(encryption_key_bytes)
|
2059
|
-
|
2060
|
-
|
2061
|
-
# Overwrite the original file with encrypted data
|
2062
|
-
with open(file_path, "wb") as f:
|
2063
|
-
f.write(encrypted_data)
|
2086
|
+
content_bytes = box.encrypt(content_bytes)
|
2064
2087
|
else:
|
2065
2088
|
# Fallback to the original encryption system if key_storage is not available
|
2066
2089
|
if not self.encryption_available:
|
@@ -2068,16 +2091,8 @@ class IPFSClient:
|
|
2068
2091
|
"Encryption requested but not available. Either install key storage with 'pip install hippius_sdk[key_storage]' or configure an encryption key with 'hippius keygen --save'"
|
2069
2092
|
)
|
2070
2093
|
|
2071
|
-
# Read file content into memory
|
2072
|
-
with open(file_path, "rb") as f:
|
2073
|
-
file_data = f.read()
|
2074
|
-
|
2075
2094
|
# Encrypt the data using the client's encryption key
|
2076
|
-
|
2077
|
-
|
2078
|
-
# Overwrite the original file with encrypted data
|
2079
|
-
with open(file_path, "wb") as f:
|
2080
|
-
f.write(encrypted_data)
|
2095
|
+
content_bytes = self.encrypt_data(content_bytes)
|
2081
2096
|
|
2082
2097
|
# Store the encryption key for the result
|
2083
2098
|
encryption_key_used = (
|
@@ -2089,12 +2104,15 @@ class IPFSClient:
|
|
2089
2104
|
# Step 1: Upload to store_node (local) for immediate availability
|
2090
2105
|
try:
|
2091
2106
|
store_client = AsyncIPFSClient(api_url=store_node)
|
2092
|
-
result = await store_client.
|
2107
|
+
result = await store_client.add_bytes(
|
2108
|
+
content_bytes,
|
2109
|
+
filename=filename,
|
2110
|
+
)
|
2093
2111
|
cid = result["Hash"]
|
2094
|
-
logger.info(f"
|
2112
|
+
logger.info(f"Content uploaded to store node {store_node} with CID: {cid}")
|
2095
2113
|
except Exception as e:
|
2096
2114
|
raise HippiusIPFSError(
|
2097
|
-
f"Failed to upload
|
2115
|
+
f"Failed to upload content to store node {store_node}: {str(e)}"
|
2098
2116
|
)
|
2099
2117
|
|
2100
2118
|
# Step 2: Pin to pin_node (remote) for persistence and backup
|
@@ -2104,7 +2122,7 @@ class IPFSClient:
|
|
2104
2122
|
logger.info(f"File pinned to backup node {pin_node}")
|
2105
2123
|
except Exception as e:
|
2106
2124
|
raise HippiusIPFSError(
|
2107
|
-
f"Failed to pin
|
2125
|
+
f"Failed to pin content to pin node {pin_node}: {str(e)}"
|
2108
2126
|
)
|
2109
2127
|
|
2110
2128
|
# Conditionally publish to substrate marketplace based on publish flag
|
@@ -2112,7 +2130,8 @@ class IPFSClient:
|
|
2112
2130
|
try:
|
2113
2131
|
# Pass the seed phrase directly to avoid password prompts for encrypted config
|
2114
2132
|
substrate_client = SubstrateClient(
|
2115
|
-
seed_phrase=seed_phrase,
|
2133
|
+
seed_phrase=seed_phrase,
|
2134
|
+
url=substrate_url,
|
2116
2135
|
)
|
2117
2136
|
logger.info(
|
2118
2137
|
f"Submitting storage request to substrate for file: {filename}, CID: {cid}"
|
@@ -2161,7 +2180,7 @@ class IPFSClient:
|
|
2161
2180
|
return S3PublishPin(
|
2162
2181
|
cid=cid,
|
2163
2182
|
subaccount=subaccount_id,
|
2164
|
-
file_path=
|
2183
|
+
file_path=filename,
|
2165
2184
|
pin_node=pin_node,
|
2166
2185
|
substrate_url=substrate_url,
|
2167
2186
|
)
|
@@ -2169,49 +2188,80 @@ class IPFSClient:
|
|
2169
2188
|
async def s3_download(
|
2170
2189
|
self,
|
2171
2190
|
cid: str,
|
2172
|
-
output_path: str,
|
2173
|
-
subaccount_id: str,
|
2174
|
-
bucket_name: str,
|
2191
|
+
output_path: Optional[str] = None,
|
2192
|
+
subaccount_id: Optional[str] = None,
|
2193
|
+
bucket_name: Optional[str] = None,
|
2175
2194
|
auto_decrypt: bool = True,
|
2176
2195
|
download_node: str = "http://localhost:5001",
|
2177
|
-
|
2196
|
+
return_bytes: bool = False,
|
2197
|
+
streaming: bool = False,
|
2198
|
+
) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
|
2178
2199
|
"""
|
2179
|
-
Download
|
2200
|
+
Download content from IPFS with flexible output options and automatic decryption.
|
2180
2201
|
|
2181
|
-
This method
|
2182
|
-
|
2183
|
-
|
2184
|
-
|
2185
|
-
- Falls back to client encryption key if key storage is not available
|
2186
|
-
- Returns the file in decrypted form if decryption succeeds
|
2202
|
+
This method provides multiple output modes:
|
2203
|
+
1. File output: Downloads to specified path (default mode)
|
2204
|
+
2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
|
2205
|
+
3. Streaming output: Returns raw streaming iterator from IPFS node (streaming=True)
|
2187
2206
|
|
2188
2207
|
Args:
|
2189
2208
|
cid: Content Identifier (CID) of the file to download
|
2190
|
-
output_path: Path where the downloaded file will be saved
|
2191
|
-
subaccount_id: The subaccount/account identifier
|
2192
|
-
bucket_name: The bucket name for key isolation
|
2209
|
+
output_path: Path where the downloaded file will be saved (None for bytes/streaming)
|
2210
|
+
subaccount_id: The subaccount/account identifier (required for decryption)
|
2211
|
+
bucket_name: The bucket name for key isolation (required for decryption)
|
2193
2212
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
2194
2213
|
download_node: IPFS node URL for download (default: local node)
|
2214
|
+
return_bytes: If True, return bytes instead of saving to file
|
2215
|
+
streaming: If True, return raw streaming iterator from IPFS (no decryption)
|
2195
2216
|
|
2196
2217
|
Returns:
|
2197
|
-
S3DownloadResult:
|
2218
|
+
S3DownloadResult: Download info and decryption status (default)
|
2219
|
+
bytes: Raw decrypted content when return_bytes=True
|
2220
|
+
AsyncIterator[bytes]: Raw streaming iterator when streaming=True
|
2198
2221
|
|
2199
2222
|
Raises:
|
2200
2223
|
HippiusIPFSError: If IPFS download fails
|
2201
2224
|
FileNotFoundError: If the output directory doesn't exist
|
2202
|
-
ValueError: If decryption fails
|
2225
|
+
ValueError: If decryption fails or invalid parameter combinations
|
2203
2226
|
"""
|
2227
|
+
# Validate parameter combinations
|
2228
|
+
if streaming and return_bytes:
|
2229
|
+
raise ValueError("Cannot specify both streaming and return_bytes")
|
2230
|
+
|
2231
|
+
if streaming and (auto_decrypt or subaccount_id or bucket_name):
|
2232
|
+
logger.warning(
|
2233
|
+
"streaming=True ignores decryption parameters - returns raw encrypted stream"
|
2234
|
+
)
|
2235
|
+
|
2236
|
+
if streaming:
|
2237
|
+
# Return raw streaming iterator from IPFS node - no processing
|
2238
|
+
# _get_ipfs_stream is an async generator, return it directly
|
2239
|
+
async def streaming_wrapper():
|
2240
|
+
async for chunk in self._get_ipfs_stream(cid, download_node):
|
2241
|
+
yield chunk
|
2242
|
+
return streaming_wrapper()
|
2243
|
+
|
2204
2244
|
start_time = time.time()
|
2205
2245
|
|
2246
|
+
# Validate required parameters for decryption
|
2247
|
+
if auto_decrypt and (not subaccount_id or not bucket_name):
|
2248
|
+
raise ValueError(
|
2249
|
+
"subaccount_id and bucket_name are required when auto_decrypt=True"
|
2250
|
+
)
|
2251
|
+
|
2252
|
+
if not return_bytes and not output_path:
|
2253
|
+
raise ValueError("output_path is required when not using return_bytes mode")
|
2254
|
+
|
2206
2255
|
# Download the file directly into memory from the specified download_node
|
2207
2256
|
try:
|
2208
|
-
# Create parent directories if they don't exist
|
2209
|
-
|
2257
|
+
# Create parent directories if they don't exist (only for file output mode)
|
2258
|
+
if not return_bytes:
|
2259
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
2210
2260
|
|
2211
2261
|
download_client = AsyncIPFSClient(api_url=download_node)
|
2212
2262
|
|
2213
2263
|
download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
|
2214
|
-
|
2264
|
+
|
2215
2265
|
# Download file into memory
|
2216
2266
|
file_data = bytearray()
|
2217
2267
|
async with download_client.client.stream("POST", download_url) as response:
|
@@ -2221,7 +2271,9 @@ class IPFSClient:
|
|
2221
2271
|
|
2222
2272
|
# Convert to bytes for consistency
|
2223
2273
|
file_data = bytes(file_data)
|
2224
|
-
logger.info(
|
2274
|
+
logger.info(
|
2275
|
+
f"File downloaded from {download_node} with CID: {cid} ({len(file_data)} bytes)"
|
2276
|
+
)
|
2225
2277
|
|
2226
2278
|
except Exception as e:
|
2227
2279
|
raise HippiusIPFSError(
|
@@ -2299,8 +2351,7 @@ class IPFSClient:
|
|
2299
2351
|
except Exception as decrypt_error:
|
2300
2352
|
logger.debug(
|
2301
2353
|
f"Decryption failed with stored key: {decrypt_error}"
|
2302
|
-
)
|
2303
|
-
# Continue to try fallback decryption
|
2354
|
+
) # Continue to try fallback decryption
|
2304
2355
|
else:
|
2305
2356
|
logger.debug(
|
2306
2357
|
"No encryption key found for account+bucket combination"
|
@@ -2343,20 +2394,58 @@ class IPFSClient:
|
|
2343
2394
|
elif not decryption_attempted:
|
2344
2395
|
logger.debug("No decryption attempted - no keys available")
|
2345
2396
|
|
2346
|
-
#
|
2347
|
-
|
2348
|
-
|
2397
|
+
# Handle output based on mode
|
2398
|
+
if return_bytes:
|
2399
|
+
# Return bytes directly
|
2400
|
+
return final_data
|
2401
|
+
else:
|
2402
|
+
# Write the final data (encrypted or decrypted) to disk once
|
2403
|
+
with open(output_path, "wb") as f:
|
2404
|
+
f.write(final_data)
|
2405
|
+
|
2406
|
+
# Get final file info
|
2407
|
+
size_bytes = len(final_data)
|
2408
|
+
elapsed_time = time.time() - start_time
|
2349
2409
|
|
2350
|
-
|
2351
|
-
|
2352
|
-
|
2410
|
+
return S3DownloadResult(
|
2411
|
+
cid=cid,
|
2412
|
+
output_path=output_path,
|
2413
|
+
size_bytes=size_bytes,
|
2414
|
+
size_formatted=self.format_size(size_bytes),
|
2415
|
+
elapsed_seconds=round(elapsed_time, 2),
|
2416
|
+
decrypted=decrypted,
|
2417
|
+
encryption_key=encryption_key_used,
|
2418
|
+
)
|
2353
2419
|
|
2354
|
-
|
2355
|
-
|
2356
|
-
|
2357
|
-
|
2358
|
-
|
2359
|
-
|
2360
|
-
|
2361
|
-
|
2362
|
-
|
2420
|
+
async def _get_ipfs_stream(
|
2421
|
+
self, cid: str, download_node: str
|
2422
|
+
) -> AsyncIterator[bytes]:
|
2423
|
+
"""
|
2424
|
+
Get a raw streaming iterator from IPFS node.
|
2425
|
+
|
2426
|
+
This method returns the raw encrypted stream directly from the IPFS node
|
2427
|
+
without any processing, decryption, or temporary file operations.
|
2428
|
+
|
2429
|
+
Args:
|
2430
|
+
cid: Content Identifier (CID) of the file to stream
|
2431
|
+
download_node: IPFS node URL for download
|
2432
|
+
|
2433
|
+
Returns:
|
2434
|
+
AsyncIterator[bytes]: Raw streaming iterator from IPFS
|
2435
|
+
|
2436
|
+
Raises:
|
2437
|
+
HippiusIPFSError: If IPFS stream fails
|
2438
|
+
"""
|
2439
|
+
try:
|
2440
|
+
download_client = AsyncIPFSClient(api_url=download_node)
|
2441
|
+
download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
|
2442
|
+
|
2443
|
+
async with download_client.client.stream("POST", download_url) as response:
|
2444
|
+
response.raise_for_status()
|
2445
|
+
logger.info(f"Started streaming from {download_node} for CID: {cid}")
|
2446
|
+
|
2447
|
+
async for chunk in response.aiter_bytes(chunk_size=8192):
|
2448
|
+
yield chunk
|
2449
|
+
|
2450
|
+
except Exception as e:
|
2451
|
+
raise HippiusIPFSError(f"Failed to stream from {download_node}: {str(e)}")
|
hippius_sdk/ipfs_core.py
CHANGED
@@ -48,11 +48,16 @@ class AsyncIPFSClient:
|
|
48
48
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
49
49
|
await self.close()
|
50
50
|
|
51
|
-
async def add_file(
|
51
|
+
async def add_file(
|
52
|
+
self,
|
53
|
+
file_path: str,
|
54
|
+
file_name: str = None,
|
55
|
+
) -> Dict[str, Any]:
|
52
56
|
"""
|
53
57
|
Add a file to IPFS.
|
54
58
|
|
55
59
|
Args:
|
60
|
+
file_name: Name of the file
|
56
61
|
file_path: Path to the file to add
|
57
62
|
|
58
63
|
Returns:
|
@@ -60,9 +65,9 @@ class AsyncIPFSClient:
|
|
60
65
|
"""
|
61
66
|
with open(file_path, "rb") as f:
|
62
67
|
file_content = f.read()
|
63
|
-
|
64
|
-
|
65
|
-
|
68
|
+
file_name = file_name if file_name else os.path.basename(file_path)
|
69
|
+
files = {"file": (file_name, file_content, "application/octet-stream")}
|
70
|
+
|
66
71
|
# Explicitly set wrap-with-directory=false to prevent wrapping in directory
|
67
72
|
response = await self.client.post(
|
68
73
|
f"{self.api_url}/api/v0/add?wrap-with-directory=false&cid-version=1",
|
File without changes
|
File without changes
|