hippius 0.2.40__tar.gz → 0.2.42__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.
- {hippius-0.2.40 → hippius-0.2.42}/PKG-INFO +1 -1
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/__init__.py +1 -1
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/client.py +42 -30
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/ipfs.py +214 -73
- {hippius-0.2.40 → hippius-0.2.42}/pyproject.toml +1 -1
- {hippius-0.2.40 → hippius-0.2.42}/README.md +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/cli.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/cli_assets.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/cli_handlers.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/cli_parser.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/cli_rich.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/config.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db/README.md +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db/env.db.template +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db/migrations/20241201000001_create_key_storage_tables.sql +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db/migrations/20241202000001_switch_to_subaccount_encryption.sql +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db/setup_database.sh +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/db_utils.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/errors.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/ipfs_core.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/key_storage.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/substrate.py +0 -0
- {hippius-0.2.40 → hippius-0.2.42}/hippius_sdk/utils.py +0 -0
@@ -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.42"
|
30
30
|
__all__ = [
|
31
31
|
"HippiusClient",
|
32
32
|
"IPFSClient",
|
@@ -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
|
@@ -519,7 +520,7 @@ class HippiusClient:
|
|
519
520
|
|
520
521
|
async def s3_publish(
|
521
522
|
self,
|
522
|
-
|
523
|
+
content: Union[str, bytes, os.PathLike],
|
523
524
|
encrypt: bool,
|
524
525
|
seed_phrase: str,
|
525
526
|
subaccount_id: str,
|
@@ -531,15 +532,15 @@ class HippiusClient:
|
|
531
532
|
publish: bool = True,
|
532
533
|
) -> Union[S3PublishResult, S3PublishPin]:
|
533
534
|
"""
|
534
|
-
Publish
|
535
|
+
Publish content to IPFS and the Hippius marketplace in one operation.
|
535
536
|
|
536
537
|
Uses a two-node architecture for optimal performance:
|
537
538
|
1. Uploads to store_node (local) for immediate availability
|
538
539
|
2. Pins to pin_node (remote) for persistence and backup
|
539
540
|
|
540
541
|
Args:
|
541
|
-
|
542
|
-
|
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)
|
543
544
|
encrypt: Whether to encrypt the file before uploading
|
544
545
|
seed_phrase: Seed phrase for blockchain transaction signing
|
545
546
|
subaccount_id: The subaccount/account identifier
|
@@ -560,47 +561,51 @@ class HippiusClient:
|
|
560
561
|
ValueError: If encryption is requested but not available
|
561
562
|
"""
|
562
563
|
return await self.ipfs_client.s3_publish(
|
563
|
-
|
564
|
-
encrypt,
|
565
|
-
seed_phrase,
|
566
|
-
subaccount_id,
|
567
|
-
bucket_name,
|
568
|
-
store_node,
|
569
|
-
pin_node,
|
570
|
-
substrate_url,
|
571
|
-
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,
|
572
573
|
file_name=file_name,
|
573
574
|
)
|
574
575
|
|
575
576
|
async def s3_download(
|
576
577
|
self,
|
577
578
|
cid: str,
|
578
|
-
output_path: str,
|
579
|
-
subaccount_id: str,
|
580
|
-
bucket_name: str,
|
579
|
+
output_path: Optional[str] = None,
|
580
|
+
subaccount_id: Optional[str] = None,
|
581
|
+
bucket_name: Optional[str] = None,
|
581
582
|
auto_decrypt: bool = True,
|
582
583
|
download_node: str = "http://localhost:5001",
|
583
|
-
|
584
|
+
return_bytes: bool = False,
|
585
|
+
streaming: bool = False,
|
586
|
+
) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
|
584
587
|
"""
|
585
|
-
Download
|
588
|
+
Download content from IPFS with flexible output options and automatic decryption.
|
586
589
|
|
587
|
-
This method
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
- Falls back to client encryption key if key storage is not available
|
592
|
-
- 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)
|
593
594
|
|
594
595
|
Args:
|
595
596
|
cid: Content Identifier (CID) of the file to download
|
596
|
-
output_path: Path where the downloaded file will be saved
|
597
|
-
subaccount_id: The subaccount/account identifier
|
598
|
-
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)
|
599
600
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
600
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 decrypted bytes when auto_decrypt=True, or raw streaming iterator when auto_decrypt=False
|
601
604
|
|
602
605
|
Returns:
|
603
|
-
S3DownloadResult:
|
606
|
+
S3DownloadResult: Download info and decryption status (default)
|
607
|
+
bytes: Raw decrypted content when return_bytes=True or streaming=True with auto_decrypt=True
|
608
|
+
AsyncIterator[bytes]: Raw streaming iterator when streaming=True and auto_decrypt=False
|
604
609
|
|
605
610
|
Raises:
|
606
611
|
HippiusIPFSError: If IPFS download fails
|
@@ -608,5 +613,12 @@ class HippiusClient:
|
|
608
613
|
ValueError: If decryption fails
|
609
614
|
"""
|
610
615
|
return await self.ipfs_client.s3_download(
|
611
|
-
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,
|
612
624
|
)
|
@@ -13,7 +13,7 @@ import shutil
|
|
13
13
|
import tempfile
|
14
14
|
import time
|
15
15
|
import uuid
|
16
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Union, AsyncIterator
|
17
17
|
|
18
18
|
import httpx
|
19
19
|
from pydantic import BaseModel
|
@@ -69,6 +69,7 @@ class S3PublishPin(BaseModel):
|
|
69
69
|
cid: str
|
70
70
|
subaccount: str
|
71
71
|
file_path: str
|
72
|
+
file_name: str
|
72
73
|
pin_node: str
|
73
74
|
substrate_url: str
|
74
75
|
|
@@ -1967,7 +1968,7 @@ class IPFSClient:
|
|
1967
1968
|
|
1968
1969
|
async def s3_publish(
|
1969
1970
|
self,
|
1970
|
-
|
1971
|
+
content: Union[str, bytes, os.PathLike],
|
1971
1972
|
encrypt: bool,
|
1972
1973
|
seed_phrase: str,
|
1973
1974
|
subaccount_id: str,
|
@@ -1979,7 +1980,7 @@ class IPFSClient:
|
|
1979
1980
|
file_name: str = None,
|
1980
1981
|
) -> Union[S3PublishResult, S3PublishPin]:
|
1981
1982
|
"""
|
1982
|
-
Publish
|
1983
|
+
Publish content to IPFS and the Hippius marketplace in one operation.
|
1983
1984
|
|
1984
1985
|
This method uses a two-node architecture for optimal performance:
|
1985
1986
|
1. Uploads to store_node (local) for immediate availability
|
@@ -1992,8 +1993,8 @@ class IPFSClient:
|
|
1992
1993
|
- Always uses the most recent key for an account+bucket combination
|
1993
1994
|
|
1994
1995
|
Args:
|
1995
|
-
|
1996
|
-
encrypt: Whether to encrypt the
|
1996
|
+
content: Either a file path (str/PathLike) or bytes content to publish
|
1997
|
+
encrypt: Whether to encrypt the content before uploading
|
1997
1998
|
seed_phrase: Seed phrase for blockchain transaction signing
|
1998
1999
|
subaccount_id: The subaccount/account identifier
|
1999
2000
|
bucket_name: The bucket name for key isolation
|
@@ -2001,25 +2002,47 @@ class IPFSClient:
|
|
2001
2002
|
pin_node: IPFS node URL for backup pinning (default: remote service)
|
2002
2003
|
substrate_url: the substrate url to connect to for the storage request
|
2003
2004
|
publish: Whether to publish to blockchain (True) or just upload to IPFS (False)
|
2004
|
-
file_name: The original file name
|
2005
|
+
file_name: The original file name (required if content is bytes)
|
2005
2006
|
|
2006
2007
|
Returns:
|
2007
2008
|
S3PublishResult: Object containing CID, file info, and transaction hash when publish=True
|
2008
|
-
S3PublishPin: Object containing CID, subaccount,
|
2009
|
+
S3PublishPin: Object containing CID, subaccount, content info, pin_node, substrate_url when publish=False
|
2009
2010
|
|
2010
2011
|
Raises:
|
2011
2012
|
HippiusIPFSError: If IPFS operations (add or pin) fail
|
2012
2013
|
HippiusSubstrateError: If substrate call fails
|
2013
|
-
FileNotFoundError: If the file doesn't exist
|
2014
|
-
ValueError: If encryption is requested but not available
|
2014
|
+
FileNotFoundError: If the file doesn't exist (when content is a path)
|
2015
|
+
ValueError: If encryption is requested but not available, or if file_name is missing for bytes content
|
2015
2016
|
"""
|
2016
|
-
#
|
2017
|
-
|
2018
|
-
|
2017
|
+
# Determine if content is a file path or bytes
|
2018
|
+
is_file_path = isinstance(content, (str, os.PathLike))
|
2019
|
+
|
2020
|
+
if is_file_path:
|
2021
|
+
# Handle file path input
|
2022
|
+
file_path = str(content)
|
2023
|
+
if not os.path.exists(file_path):
|
2024
|
+
raise FileNotFoundError(f"File {file_path} not found")
|
2025
|
+
|
2026
|
+
# Get file info
|
2027
|
+
filename = file_name or os.path.basename(file_path)
|
2028
|
+
size_bytes = os.path.getsize(file_path)
|
2029
|
+
|
2030
|
+
# Read file content into memory
|
2031
|
+
with open(file_path, "rb") as f:
|
2032
|
+
content_bytes = f.read()
|
2033
|
+
else:
|
2034
|
+
# Handle bytes input
|
2035
|
+
if not isinstance(content, bytes):
|
2036
|
+
raise ValueError(
|
2037
|
+
f"Content must be str, PathLike, or bytes, got {type(content)}"
|
2038
|
+
)
|
2019
2039
|
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
2040
|
+
if not file_name:
|
2041
|
+
raise ValueError("file_name is required when content is bytes")
|
2042
|
+
|
2043
|
+
filename = file_name
|
2044
|
+
content_bytes = content
|
2045
|
+
size_bytes = len(content_bytes)
|
2023
2046
|
|
2024
2047
|
# Handle encryption if requested with automatic key management
|
2025
2048
|
encryption_key_used = None
|
@@ -2057,19 +2080,11 @@ class IPFSClient:
|
|
2057
2080
|
encryption_key_bytes = base64.b64decode(new_key_b64)
|
2058
2081
|
encryption_key_used = new_key_b64
|
2059
2082
|
|
2060
|
-
# Read file content into memory
|
2061
|
-
with open(file_path, "rb") as f:
|
2062
|
-
file_data = f.read()
|
2063
|
-
|
2064
2083
|
# Encrypt the data using the key from key storage
|
2065
2084
|
import nacl.secret
|
2066
2085
|
|
2067
2086
|
box = nacl.secret.SecretBox(encryption_key_bytes)
|
2068
|
-
|
2069
|
-
|
2070
|
-
# Overwrite the original file with encrypted data
|
2071
|
-
with open(file_path, "wb") as f:
|
2072
|
-
f.write(encrypted_data)
|
2087
|
+
content_bytes = box.encrypt(content_bytes)
|
2073
2088
|
else:
|
2074
2089
|
# Fallback to the original encryption system if key_storage is not available
|
2075
2090
|
if not self.encryption_available:
|
@@ -2077,16 +2092,8 @@ class IPFSClient:
|
|
2077
2092
|
"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'"
|
2078
2093
|
)
|
2079
2094
|
|
2080
|
-
# Read file content into memory
|
2081
|
-
with open(file_path, "rb") as f:
|
2082
|
-
file_data = f.read()
|
2083
|
-
|
2084
2095
|
# Encrypt the data using the client's encryption key
|
2085
|
-
|
2086
|
-
|
2087
|
-
# Overwrite the original file with encrypted data
|
2088
|
-
with open(file_path, "wb") as f:
|
2089
|
-
f.write(encrypted_data)
|
2096
|
+
content_bytes = self.encrypt_data(content_bytes)
|
2090
2097
|
|
2091
2098
|
# Store the encryption key for the result
|
2092
2099
|
encryption_key_used = (
|
@@ -2098,15 +2105,15 @@ class IPFSClient:
|
|
2098
2105
|
# Step 1: Upload to store_node (local) for immediate availability
|
2099
2106
|
try:
|
2100
2107
|
store_client = AsyncIPFSClient(api_url=store_node)
|
2101
|
-
result = await store_client.
|
2102
|
-
|
2103
|
-
|
2108
|
+
result = await store_client.add_bytes(
|
2109
|
+
content_bytes,
|
2110
|
+
filename=filename,
|
2104
2111
|
)
|
2105
2112
|
cid = result["Hash"]
|
2106
|
-
logger.info(f"
|
2113
|
+
logger.info(f"Content uploaded to store node {store_node} with CID: {cid}")
|
2107
2114
|
except Exception as e:
|
2108
2115
|
raise HippiusIPFSError(
|
2109
|
-
f"Failed to upload
|
2116
|
+
f"Failed to upload content to store node {store_node}: {str(e)}"
|
2110
2117
|
)
|
2111
2118
|
|
2112
2119
|
# Step 2: Pin to pin_node (remote) for persistence and backup
|
@@ -2116,7 +2123,7 @@ class IPFSClient:
|
|
2116
2123
|
logger.info(f"File pinned to backup node {pin_node}")
|
2117
2124
|
except Exception as e:
|
2118
2125
|
raise HippiusIPFSError(
|
2119
|
-
f"Failed to pin
|
2126
|
+
f"Failed to pin content to pin node {pin_node}: {str(e)}"
|
2120
2127
|
)
|
2121
2128
|
|
2122
2129
|
# Conditionally publish to substrate marketplace based on publish flag
|
@@ -2174,7 +2181,8 @@ class IPFSClient:
|
|
2174
2181
|
return S3PublishPin(
|
2175
2182
|
cid=cid,
|
2176
2183
|
subaccount=subaccount_id,
|
2177
|
-
file_path=
|
2184
|
+
file_path=filename,
|
2185
|
+
file_name=filename,
|
2178
2186
|
pin_node=pin_node,
|
2179
2187
|
substrate_url=substrate_url,
|
2180
2188
|
)
|
@@ -2182,44 +2190,139 @@ class IPFSClient:
|
|
2182
2190
|
async def s3_download(
|
2183
2191
|
self,
|
2184
2192
|
cid: str,
|
2185
|
-
output_path: str,
|
2186
|
-
subaccount_id: str,
|
2187
|
-
bucket_name: str,
|
2193
|
+
output_path: Optional[str] = None,
|
2194
|
+
subaccount_id: Optional[str] = None,
|
2195
|
+
bucket_name: Optional[str] = None,
|
2188
2196
|
auto_decrypt: bool = True,
|
2189
2197
|
download_node: str = "http://localhost:5001",
|
2190
|
-
|
2198
|
+
return_bytes: bool = False,
|
2199
|
+
streaming: bool = False,
|
2200
|
+
) -> Union[S3DownloadResult, bytes, AsyncIterator[bytes]]:
|
2191
2201
|
"""
|
2192
|
-
Download
|
2202
|
+
Download content from IPFS with flexible output options and automatic decryption.
|
2193
2203
|
|
2194
|
-
This method
|
2195
|
-
|
2196
|
-
|
2197
|
-
|
2198
|
-
- Falls back to client encryption key if key storage is not available
|
2199
|
-
- Returns the file in decrypted form if decryption succeeds
|
2204
|
+
This method provides multiple output modes:
|
2205
|
+
1. File output: Downloads to specified path (default mode)
|
2206
|
+
2. Bytes output: Returns decrypted bytes in memory (return_bytes=True)
|
2207
|
+
3. Streaming output: Returns decrypted bytes or raw iterator based on auto_decrypt (streaming=True)
|
2200
2208
|
|
2201
2209
|
Args:
|
2202
2210
|
cid: Content Identifier (CID) of the file to download
|
2203
|
-
output_path: Path where the downloaded file will be saved
|
2204
|
-
subaccount_id: The subaccount/account identifier
|
2205
|
-
bucket_name: The bucket name for key isolation
|
2211
|
+
output_path: Path where the downloaded file will be saved (None for bytes/streaming)
|
2212
|
+
subaccount_id: The subaccount/account identifier (required for decryption)
|
2213
|
+
bucket_name: The bucket name for key isolation (required for decryption)
|
2206
2214
|
auto_decrypt: Whether to attempt automatic decryption (default: True)
|
2207
2215
|
download_node: IPFS node URL for download (default: local node)
|
2216
|
+
return_bytes: If True, return bytes instead of saving to file
|
2217
|
+
streaming: If True, return decrypted bytes when auto_decrypt=True, or raw streaming iterator when auto_decrypt=False
|
2208
2218
|
|
2209
2219
|
Returns:
|
2210
|
-
S3DownloadResult:
|
2220
|
+
S3DownloadResult: Download info and decryption status (default)
|
2221
|
+
bytes: Raw decrypted content when return_bytes=True or streaming=True with auto_decrypt=True
|
2222
|
+
AsyncIterator[bytes]: Raw streaming iterator when streaming=True and auto_decrypt=False
|
2211
2223
|
|
2212
2224
|
Raises:
|
2213
2225
|
HippiusIPFSError: If IPFS download fails
|
2214
2226
|
FileNotFoundError: If the output directory doesn't exist
|
2215
|
-
ValueError: If decryption fails
|
2227
|
+
ValueError: If decryption fails or invalid parameter combinations
|
2216
2228
|
"""
|
2229
|
+
# Validate parameter combinations
|
2230
|
+
if streaming and return_bytes:
|
2231
|
+
raise ValueError("Cannot specify both streaming and return_bytes")
|
2232
|
+
|
2233
|
+
if streaming:
|
2234
|
+
# Validate required parameters for decryption if auto_decrypt is True
|
2235
|
+
if auto_decrypt and (not subaccount_id or not bucket_name):
|
2236
|
+
raise ValueError(
|
2237
|
+
"subaccount_id and bucket_name are required for streaming decryption"
|
2238
|
+
)
|
2239
|
+
|
2240
|
+
if auto_decrypt:
|
2241
|
+
# Return decrypted bytes directly (not streaming)
|
2242
|
+
try:
|
2243
|
+
key_storage_available = is_key_storage_enabled()
|
2244
|
+
except ImportError:
|
2245
|
+
key_storage_available = False
|
2246
|
+
|
2247
|
+
encryption_key_bytes = None
|
2248
|
+
|
2249
|
+
if key_storage_available:
|
2250
|
+
# Create combined key identifier from account+bucket
|
2251
|
+
account_bucket_key = f"{subaccount_id}:{bucket_name}"
|
2252
|
+
|
2253
|
+
try:
|
2254
|
+
existing_key_b64 = await get_key_for_subaccount(
|
2255
|
+
account_bucket_key
|
2256
|
+
)
|
2257
|
+
if existing_key_b64:
|
2258
|
+
encryption_key_bytes = base64.b64decode(existing_key_b64)
|
2259
|
+
except Exception as e:
|
2260
|
+
logger.debug(f"Failed to get encryption key: {e}")
|
2261
|
+
|
2262
|
+
# If key storage decryption failed or wasn't available, try client encryption key
|
2263
|
+
if not encryption_key_bytes and self.encryption_available:
|
2264
|
+
logger.debug("Using client encryption key for streaming decryption")
|
2265
|
+
encryption_key_bytes = self.encryption_key
|
2266
|
+
|
2267
|
+
if not encryption_key_bytes:
|
2268
|
+
logger.warning(
|
2269
|
+
"No encryption key found - downloading raw encrypted data as bytes"
|
2270
|
+
)
|
2271
|
+
# Return raw encrypted data as bytes
|
2272
|
+
encrypted_data = b""
|
2273
|
+
async for chunk in self._get_ipfs_stream(cid, download_node):
|
2274
|
+
encrypted_data += chunk
|
2275
|
+
return encrypted_data
|
2276
|
+
|
2277
|
+
# Stream and decrypt the content using hybrid buffered approach
|
2278
|
+
import nacl.secret
|
2279
|
+
|
2280
|
+
# Collect all encrypted data first
|
2281
|
+
logger.debug("Buffering encrypted content for decryption")
|
2282
|
+
encrypted_data = b""
|
2283
|
+
async for chunk in self._get_ipfs_stream(cid, download_node):
|
2284
|
+
encrypted_data += chunk
|
2285
|
+
|
2286
|
+
# Decrypt the complete buffered content and return as bytes
|
2287
|
+
try:
|
2288
|
+
box = nacl.secret.SecretBox(encryption_key_bytes)
|
2289
|
+
decrypted_data = box.decrypt(encrypted_data)
|
2290
|
+
logger.info(f"Successfully decrypted {len(decrypted_data)} bytes")
|
2291
|
+
|
2292
|
+
# Return all decrypted data as bytes
|
2293
|
+
return decrypted_data
|
2294
|
+
|
2295
|
+
except Exception as decrypt_error:
|
2296
|
+
logger.error(f"Streaming decryption failed: {decrypt_error}")
|
2297
|
+
raise ValueError(
|
2298
|
+
f"Failed to decrypt streaming content: {decrypt_error}"
|
2299
|
+
)
|
2300
|
+
else:
|
2301
|
+
# Return raw streaming iterator from IPFS node - no processing
|
2302
|
+
async def streaming_wrapper():
|
2303
|
+
async for chunk in self._get_ipfs_stream(cid, download_node):
|
2304
|
+
yield chunk
|
2305
|
+
|
2306
|
+
return streaming_wrapper()
|
2307
|
+
|
2217
2308
|
start_time = time.time()
|
2218
2309
|
|
2310
|
+
# Validate required parameters for decryption
|
2311
|
+
if auto_decrypt and (not subaccount_id or not bucket_name):
|
2312
|
+
raise ValueError(
|
2313
|
+
"subaccount_id and bucket_name are required when auto_decrypt=True"
|
2314
|
+
)
|
2315
|
+
|
2316
|
+
if not return_bytes and not output_path:
|
2317
|
+
raise ValueError("output_path is required when not using return_bytes mode")
|
2318
|
+
|
2219
2319
|
# Download the file directly into memory from the specified download_node
|
2220
2320
|
try:
|
2221
|
-
# Create parent directories if they don't exist
|
2222
|
-
|
2321
|
+
# Create parent directories if they don't exist (only for file output mode)
|
2322
|
+
if not return_bytes:
|
2323
|
+
os.makedirs(
|
2324
|
+
os.path.dirname(os.path.abspath(output_path)), exist_ok=True
|
2325
|
+
)
|
2223
2326
|
|
2224
2327
|
download_client = AsyncIPFSClient(api_url=download_node)
|
2225
2328
|
|
@@ -2357,20 +2460,58 @@ class IPFSClient:
|
|
2357
2460
|
elif not decryption_attempted:
|
2358
2461
|
logger.debug("No decryption attempted - no keys available")
|
2359
2462
|
|
2360
|
-
#
|
2361
|
-
|
2362
|
-
|
2463
|
+
# Handle output based on mode
|
2464
|
+
if return_bytes:
|
2465
|
+
# Return bytes directly
|
2466
|
+
return final_data
|
2467
|
+
else:
|
2468
|
+
# Write the final data (encrypted or decrypted) to disk once
|
2469
|
+
with open(output_path, "wb") as f:
|
2470
|
+
f.write(final_data)
|
2363
2471
|
|
2364
|
-
|
2365
|
-
|
2366
|
-
|
2472
|
+
# Get final file info
|
2473
|
+
size_bytes = len(final_data)
|
2474
|
+
elapsed_time = time.time() - start_time
|
2367
2475
|
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2372
|
-
|
2373
|
-
|
2374
|
-
|
2375
|
-
|
2376
|
-
|
2476
|
+
return S3DownloadResult(
|
2477
|
+
cid=cid,
|
2478
|
+
output_path=output_path,
|
2479
|
+
size_bytes=size_bytes,
|
2480
|
+
size_formatted=self.format_size(size_bytes),
|
2481
|
+
elapsed_seconds=round(elapsed_time, 2),
|
2482
|
+
decrypted=decrypted,
|
2483
|
+
encryption_key=encryption_key_used,
|
2484
|
+
)
|
2485
|
+
|
2486
|
+
async def _get_ipfs_stream(
|
2487
|
+
self, cid: str, download_node: str
|
2488
|
+
) -> AsyncIterator[bytes]:
|
2489
|
+
"""
|
2490
|
+
Get a raw streaming iterator from IPFS node.
|
2491
|
+
|
2492
|
+
This method returns the raw encrypted stream directly from the IPFS node
|
2493
|
+
without any processing, decryption, or temporary file operations.
|
2494
|
+
|
2495
|
+
Args:
|
2496
|
+
cid: Content Identifier (CID) of the file to stream
|
2497
|
+
download_node: IPFS node URL for download
|
2498
|
+
|
2499
|
+
Returns:
|
2500
|
+
AsyncIterator[bytes]: Raw streaming iterator from IPFS
|
2501
|
+
|
2502
|
+
Raises:
|
2503
|
+
HippiusIPFSError: If IPFS stream fails
|
2504
|
+
"""
|
2505
|
+
try:
|
2506
|
+
download_client = AsyncIPFSClient(api_url=download_node)
|
2507
|
+
download_url = f"{download_node.rstrip('/')}/api/v0/cat?arg={cid}"
|
2508
|
+
|
2509
|
+
async with download_client.client.stream("POST", download_url) as response:
|
2510
|
+
response.raise_for_status()
|
2511
|
+
logger.info(f"Started streaming from {download_node} for CID: {cid}")
|
2512
|
+
|
2513
|
+
async for chunk in response.aiter_bytes(chunk_size=8192):
|
2514
|
+
yield chunk
|
2515
|
+
|
2516
|
+
except Exception as e:
|
2517
|
+
raise HippiusIPFSError(f"Failed to stream from {download_node}: {str(e)}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|