televault 0.1.0__py3-none-any.whl → 2.0.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.
- televault/__init__.py +1 -1
- televault/chunker.py +29 -27
- televault/cli.py +237 -90
- televault/compress.py +59 -23
- televault/config.py +16 -17
- televault/core.py +140 -203
- televault/crypto.py +26 -33
- televault/models.py +29 -30
- televault/telegram.py +136 -107
- televault/tui.py +632 -0
- televault-2.0.0.dist-info/METADATA +310 -0
- televault-2.0.0.dist-info/RECORD +14 -0
- {televault-0.1.0.dist-info → televault-2.0.0.dist-info}/entry_points.txt +1 -0
- televault-0.1.0.dist-info/METADATA +0 -242
- televault-0.1.0.dist-info/RECORD +0 -13
- {televault-0.1.0.dist-info → televault-2.0.0.dist-info}/WHEEL +0 -0
televault/compress.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Compression utilities for TeleVault - zstd for speed and ratio."""
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import BinaryIO
|
|
6
4
|
|
|
7
5
|
import zstandard as zstd
|
|
8
6
|
|
|
@@ -15,17 +13,51 @@ DEFAULT_LEVEL = 3
|
|
|
15
13
|
# File extensions that are already compressed (skip compression)
|
|
16
14
|
INCOMPRESSIBLE_EXTENSIONS = {
|
|
17
15
|
# Images
|
|
18
|
-
".jpg",
|
|
16
|
+
".jpg",
|
|
17
|
+
".jpeg",
|
|
18
|
+
".png",
|
|
19
|
+
".gif",
|
|
20
|
+
".webp",
|
|
21
|
+
".heic",
|
|
22
|
+
".heif",
|
|
23
|
+
".avif",
|
|
19
24
|
# Video
|
|
20
|
-
".mp4",
|
|
25
|
+
".mp4",
|
|
26
|
+
".mkv",
|
|
27
|
+
".avi",
|
|
28
|
+
".mov",
|
|
29
|
+
".webm",
|
|
30
|
+
".m4v",
|
|
31
|
+
".wmv",
|
|
32
|
+
".flv",
|
|
21
33
|
# Audio
|
|
22
|
-
".mp3",
|
|
34
|
+
".mp3",
|
|
35
|
+
".aac",
|
|
36
|
+
".ogg",
|
|
37
|
+
".opus",
|
|
38
|
+
".flac",
|
|
39
|
+
".m4a",
|
|
40
|
+
".wma",
|
|
23
41
|
# Archives
|
|
24
|
-
".zip",
|
|
42
|
+
".zip",
|
|
43
|
+
".gz",
|
|
44
|
+
".bz2",
|
|
45
|
+
".xz",
|
|
46
|
+
".7z",
|
|
47
|
+
".rar",
|
|
48
|
+
".zst",
|
|
49
|
+
".lz4",
|
|
50
|
+
".lzma",
|
|
25
51
|
# Documents (already compressed)
|
|
26
|
-
".pdf",
|
|
52
|
+
".pdf",
|
|
53
|
+
".docx",
|
|
54
|
+
".xlsx",
|
|
55
|
+
".pptx",
|
|
56
|
+
".odt",
|
|
27
57
|
# Other
|
|
28
|
-
".woff",
|
|
58
|
+
".woff",
|
|
59
|
+
".woff2",
|
|
60
|
+
".br",
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
|
|
@@ -49,53 +81,57 @@ def decompress_data(data: bytes, max_output_size: int = 0) -> bytes:
|
|
|
49
81
|
return dctx.decompress(data, max_output_size=max_output_size)
|
|
50
82
|
|
|
51
83
|
|
|
52
|
-
def compress_file(
|
|
84
|
+
def compress_file(
|
|
85
|
+
input_path: str | Path,
|
|
86
|
+
output_path: str | Path,
|
|
87
|
+
level: int = DEFAULT_LEVEL,
|
|
88
|
+
) -> float:
|
|
53
89
|
"""
|
|
54
90
|
Compress a file using zstd.
|
|
55
|
-
|
|
91
|
+
|
|
56
92
|
Returns compression ratio (compressed_size / original_size).
|
|
57
93
|
"""
|
|
58
94
|
cctx = zstd.ZstdCompressor(level=level)
|
|
59
|
-
|
|
95
|
+
|
|
60
96
|
with open(input_path, "rb") as fin, open(output_path, "wb") as fout:
|
|
61
97
|
cctx.copy_stream(fin, fout)
|
|
62
|
-
|
|
98
|
+
|
|
63
99
|
original_size = Path(input_path).stat().st_size
|
|
64
100
|
compressed_size = Path(output_path).stat().st_size
|
|
65
|
-
|
|
101
|
+
|
|
66
102
|
return compressed_size / original_size if original_size > 0 else 1.0
|
|
67
103
|
|
|
68
104
|
|
|
69
105
|
def decompress_file(input_path: str | Path, output_path: str | Path) -> None:
|
|
70
106
|
"""Decompress a zstd file."""
|
|
71
107
|
dctx = zstd.ZstdDecompressor()
|
|
72
|
-
|
|
108
|
+
|
|
73
109
|
with open(input_path, "rb") as fin, open(output_path, "wb") as fout:
|
|
74
110
|
dctx.copy_stream(fin, fout)
|
|
75
111
|
|
|
76
112
|
|
|
77
113
|
class StreamingCompressor:
|
|
78
114
|
"""Streaming compressor for pipeline integration."""
|
|
79
|
-
|
|
115
|
+
|
|
80
116
|
def __init__(self, level: int = DEFAULT_LEVEL):
|
|
81
117
|
self.cctx = zstd.ZstdCompressor(level=level)
|
|
82
118
|
self.compressor = self.cctx.compressobj()
|
|
83
119
|
self.total_in = 0
|
|
84
120
|
self.total_out = 0
|
|
85
|
-
|
|
121
|
+
|
|
86
122
|
def compress(self, data: bytes) -> bytes:
|
|
87
123
|
"""Compress a chunk of data."""
|
|
88
124
|
self.total_in += len(data)
|
|
89
125
|
compressed = self.compressor.compress(data)
|
|
90
126
|
self.total_out += len(compressed)
|
|
91
127
|
return compressed
|
|
92
|
-
|
|
128
|
+
|
|
93
129
|
def flush(self) -> bytes:
|
|
94
130
|
"""Flush remaining data and finalize compression."""
|
|
95
131
|
final = self.compressor.flush()
|
|
96
132
|
self.total_out += len(final)
|
|
97
133
|
return final
|
|
98
|
-
|
|
134
|
+
|
|
99
135
|
@property
|
|
100
136
|
def ratio(self) -> float:
|
|
101
137
|
"""Current compression ratio."""
|
|
@@ -106,11 +142,11 @@ class StreamingCompressor:
|
|
|
106
142
|
|
|
107
143
|
class StreamingDecompressor:
|
|
108
144
|
"""Streaming decompressor for pipeline integration."""
|
|
109
|
-
|
|
145
|
+
|
|
110
146
|
def __init__(self):
|
|
111
147
|
self.dctx = zstd.ZstdDecompressor()
|
|
112
148
|
self.decompressor = self.dctx.decompressobj()
|
|
113
|
-
|
|
149
|
+
|
|
114
150
|
def decompress(self, data: bytes) -> bytes:
|
|
115
151
|
"""Decompress a chunk of data."""
|
|
116
152
|
return self.decompressor.decompress(data)
|
|
@@ -119,15 +155,15 @@ class StreamingDecompressor:
|
|
|
119
155
|
def estimate_compressed_size(original_size: int, filename: str) -> int:
|
|
120
156
|
"""
|
|
121
157
|
Estimate compressed size based on file type.
|
|
122
|
-
|
|
158
|
+
|
|
123
159
|
Returns estimated size in bytes.
|
|
124
160
|
"""
|
|
125
161
|
if not should_compress(filename):
|
|
126
162
|
return original_size
|
|
127
|
-
|
|
163
|
+
|
|
128
164
|
# Typical compression ratios by type
|
|
129
165
|
suffix = Path(filename).suffix.lower()
|
|
130
|
-
|
|
166
|
+
|
|
131
167
|
if suffix in {".txt", ".log", ".csv", ".json", ".xml", ".html", ".md"}:
|
|
132
168
|
return int(original_size * 0.2) # Text compresses well
|
|
133
169
|
elif suffix in {".sql", ".py", ".js", ".ts", ".go", ".rs", ".c", ".cpp", ".h"}:
|
televault/config.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Configuration management for TeleVault."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from dataclasses import dataclass, field, asdict
|
|
6
|
-
from typing import Optional
|
|
7
4
|
import os
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def get_config_dir() -> Path:
|
|
@@ -13,7 +12,7 @@ def get_config_dir() -> Path:
|
|
|
13
12
|
base = Path(os.environ.get("APPDATA", "~"))
|
|
14
13
|
else: # Unix
|
|
15
14
|
base = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config"))
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
config_dir = base.expanduser() / "televault"
|
|
18
17
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
19
18
|
return config_dir
|
|
@@ -25,7 +24,7 @@ def get_data_dir() -> Path:
|
|
|
25
24
|
base = Path(os.environ.get("LOCALAPPDATA", "~"))
|
|
26
25
|
else:
|
|
27
26
|
base = Path(os.environ.get("XDG_DATA_HOME", "~/.local/share"))
|
|
28
|
-
|
|
27
|
+
|
|
29
28
|
data_dir = base.expanduser() / "televault"
|
|
30
29
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
31
30
|
return data_dir
|
|
@@ -34,44 +33,44 @@ def get_data_dir() -> Path:
|
|
|
34
33
|
@dataclass
|
|
35
34
|
class Config:
|
|
36
35
|
"""TeleVault configuration."""
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
# Telegram settings
|
|
39
|
-
channel_id:
|
|
40
|
-
|
|
38
|
+
channel_id: int | None = None
|
|
39
|
+
|
|
41
40
|
# Chunking
|
|
42
41
|
chunk_size: int = 100 * 1024 * 1024 # 100MB
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
# Processing options
|
|
45
44
|
compression: bool = True
|
|
46
45
|
encryption: bool = True
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
# Concurrency
|
|
49
48
|
parallel_uploads: int = 3
|
|
50
49
|
parallel_downloads: int = 5
|
|
51
|
-
|
|
50
|
+
|
|
52
51
|
# Retry settings
|
|
53
52
|
max_retries: int = 3
|
|
54
53
|
retry_delay: float = 1.0
|
|
55
|
-
|
|
54
|
+
|
|
56
55
|
def save(self) -> None:
|
|
57
56
|
"""Save config to file."""
|
|
58
57
|
config_path = get_config_dir() / "config.json"
|
|
59
58
|
with open(config_path, "w") as f:
|
|
60
59
|
json.dump(asdict(self), f, indent=2)
|
|
61
|
-
|
|
60
|
+
|
|
62
61
|
@classmethod
|
|
63
62
|
def load(cls) -> "Config":
|
|
64
63
|
"""Load config from file."""
|
|
65
64
|
config_path = get_config_dir() / "config.json"
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
if not config_path.exists():
|
|
68
67
|
return cls()
|
|
69
|
-
|
|
68
|
+
|
|
70
69
|
with open(config_path) as f:
|
|
71
70
|
data = json.load(f)
|
|
72
|
-
|
|
71
|
+
|
|
73
72
|
return cls(**data)
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
@classmethod
|
|
76
75
|
def load_or_create(cls) -> "Config":
|
|
77
76
|
"""Load config or create default."""
|