Skip to content

Commit 2bf0e2c

Browse files
committed
Add manual downloading to download dir, limits etc
1 parent 0c7e005 commit 2bf0e2c

6 files changed

Lines changed: 132 additions & 75 deletions

File tree

bare/main.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ const onrequest = async (p) => {
8787
case 'group_download':
8888
download_file(p.file);
8989
break;
90+
case 'save_to_downloads':
91+
const saveResult = await Storage.save_to_downloads(p.hash, p.fileName, p.topic);
92+
if (saveResult.success) {
93+
Hugin.send('file-saved-to-downloads', { hash: p.hash, filePath: saveResult.filePath, fileName: p.fileName });
94+
} else {
95+
Hugin.send('error-message', { message: saveResult.error || 'Failed to save file' });
96+
}
97+
return saveResult;
98+
9099
case 'keep_alive':
91100
break;
92101
case 'idle_status':

bare/storage.js

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ class HyperStorage {
5151
this.limit = 100000000000; //100 gb per session
5252
this.saved = 0;
5353
this.beams = [];
54-
this.downloading = new Set(); // prevents concurrent downloads of same hash
54+
this.downloading = new Set();
55+
this.savedFiles = new Set();
56+
this._purgeStarted = false;
5557
}
5658

5759
async load_drive(topic) {
@@ -65,6 +67,10 @@ class HyperStorage {
6567
const drive = new Hyperdrive(fileStore);
6668
this.add(drive, topic, fileStore);
6769
await drive.ready();
70+
if (!this._purgeStarted) {
71+
this._purgeStarted = true;
72+
this.startPurgeInterval();
73+
}
6874
}
6975

7076
loaded(topic) {
@@ -86,6 +92,57 @@ class HyperStorage {
8692
}
8793
}
8894

95+
async purgeOldFiles() {
96+
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
97+
const MAX_STORAGE = 500 * 1024 * 1024; // 500 MB
98+
const now = Date.now();
99+
let purged = 0;
100+
const allEntries = [];
101+
102+
for (const room of this.drives) {
103+
try {
104+
for await (const entry of room.drive.entries()) {
105+
const meta = entry.value?.metadata;
106+
if (!meta?.hash) continue;
107+
const size = parseInt(meta.size) || 0;
108+
const syncedAt = meta.syncedAt || meta.time || 0;
109+
allEntries.push({ hash: meta.hash, size, syncedAt, drive: room.drive });
110+
}
111+
} catch (e) {
112+
console.log('[storage.js] Purge scan error:', e);
113+
}
114+
}
115+
116+
// Pass 1: purge unsaved files older than 1 week
117+
for (const entry of allEntries) {
118+
if (this.savedFiles.has(entry.hash)) continue;
119+
if (now - entry.syncedAt > ONE_WEEK) {
120+
try { await entry.drive.del(entry.hash); purged++; entry.deleted = true; } catch (e) {}
121+
}
122+
}
123+
124+
// Pass 2: if still over storage cap, remove oldest unsaved files first
125+
const remaining = allEntries.filter(e => !e.deleted);
126+
const totalSize = remaining.reduce((sum, e) => sum + e.size, 0);
127+
if (totalSize > MAX_STORAGE) {
128+
const purgeable = remaining.filter(e => !this.savedFiles.has(e.hash));
129+
purgeable.sort((a, b) => a.syncedAt - b.syncedAt);
130+
let freed = 0;
131+
const excess = totalSize - MAX_STORAGE;
132+
for (const entry of purgeable) {
133+
if (freed >= excess) break;
134+
try { await entry.drive.del(entry.hash); purged++; freed += entry.size; } catch (e) {}
135+
}
136+
}
137+
138+
if (purged > 0) console.log(`[storage.js] Purged ${purged} files`);
139+
}
140+
141+
startPurgeInterval() {
142+
this.purgeOldFiles();
143+
setInterval(() => this.purgeOldFiles(), 60 * 60 * 1000);
144+
}
145+
89146
async load_files(topic) {
90147
const drive = this.get_drive(topic);
91148
if (!drive) return [];
@@ -166,8 +223,7 @@ class HyperStorage {
166223
if (Hugin.files.includes(meta.hash)) return;
167224
if (this.downloading.has(meta.hash)) return;
168225

169-
const [isMedia, fileType] = check_if_media(meta.fileName, meta.size);
170-
const autoSync = isMedia && (dm || Hugin.syncImages);
226+
const autoSync = dm || Hugin.syncImages;
171227

172228
if (autoSync) {
173229
console.log('[storage.js] Auto-syncing:', meta.fileName);
@@ -228,18 +284,9 @@ class HyperStorage {
228284
signature: file.signature,
229285
info: 'file-shared',
230286
type: 'file',
287+
syncedAt: Date.now(),
231288
},
232289
});
233-
const filePath = uniqueFilePath(Hugin.downloadDir, file.fileName);
234-
let writeOk = false;
235-
try {
236-
fs.writeFileSync(filePath, buf);
237-
writeOk = true;
238-
console.log('[storage.js] File written to:', filePath);
239-
} catch (e) {
240-
console.log('[storage.js] ERROR writing file to downloadDir:', filePath, e);
241-
}
242-
if (!writeOk) { this.downloading.delete(file.hash); return; }
243290
Hugin.files.push(file.hash);
244291
this.downloading.delete(file.hash);
245292
Hugin.send('download-file-progress', {
@@ -257,18 +304,34 @@ class HyperStorage {
257304
time: file.time,
258305
size: file.size,
259306
topic,
260-
filePath,
307+
filePath: 'storage',
261308
roomKey,
262309
dm,
263310
});
264-
if (dm) this.done(file, topic, roomKey, dm, filePath);
311+
if (dm) this.done(file, topic, roomKey, dm, 'storage');
265312
console.log('File saved from peer:', file.fileName);
266313
} catch (e) {
267314
this.downloading.delete(file.hash);
268315
console.log('Error saving file from peer:', e);
269316
}
270317
}
271318

319+
async save_to_downloads(hash, fileName, topic) {
320+
const room = this.get_room(topic);
321+
if (!room) return { success: false, error: 'Room not found' };
322+
try {
323+
const buf = await room.drive.get(hash);
324+
if (!buf) return { success: false, error: 'File not found in storage' };
325+
const filePath = uniqueFilePath(Hugin.downloadDir, fileName);
326+
fs.writeFileSync(filePath, buf);
327+
this.savedFiles.add(hash);
328+
return { success: true, filePath };
329+
} catch (e) {
330+
console.log('[storage.js] Error saving to downloads:', e);
331+
return { success: false, error: e.message };
332+
}
333+
}
334+
272335
done(file, topic, room, dm, filePath) {
273336
if (!dm) return;
274337
const [media, fileType] = check_if_media(file.fileName, file.size);

bare/swarm.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,9 +1619,8 @@ const process_files = async (data, active, con, topic) => {
16191619
if (old) continue
16201620
if (Hugin.files.some((a) => a === file.hash)) continue;
16211621
if (!check_hash(file.hash)) continue;
1622-
const [isMedia] = check_if_media(file.fileName, file.size);
16231622
await sleep(50);
1624-
if (isMedia && Hugin.syncImages) {
1623+
if (Hugin.syncImages && con.driveKey) {
16251624
request_file(con.address, topic, file, active.key);
16261625
continue;
16271626
}
@@ -1696,8 +1695,7 @@ const check_file_message = async (data, topic, address, name, dm, driveKey = nul
16961695
if (!active) return;
16971696

16981697
if (data.info === 'file-shared') {
1699-
const [isMedia] = check_if_media(data.fileName, data.size);
1700-
const autoSync = driveKey && isMedia && (Hugin.syncImages || dm);
1698+
const autoSync = driveKey && (Hugin.syncImages || dm);
17011699

17021700
if (autoSync) {
17031701
// Watcher in storage.js handles download; file-downloaded → rpc.js creates the

src/components/message-item.tsx

Lines changed: 33 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ const MessageItemInner: React.FC<Props> = ({
9696
const [actions, setActions] = useState(true);
9797
const ref = useRef<IWaveformRef>(null);
9898
const [playerState, setPlayerState] = useState(PlayerState.stopped);
99-
const [isLoading, setIsLoading] = useState(false);
10099
const [waveformError, setWaveformError] = useState(false);
101100
const lastPress = useRef<number>(0);
102101
const DOUBLE_PRESS_DELAY = 300;
@@ -204,16 +203,25 @@ const MessageItemInner: React.FC<Props> = ({
204203
: undefined,
205204
);
206205

207-
const isUndownloadedFile =
206+
const isNonMediaFile =
208207
!!file &&
209-
!file.path &&
210208
file.type === 'file' &&
211209
!imageDetails?.isImageMessage &&
212210
!audioDetails?.isAudioMessage &&
213211
!videoDetails.isVideoMessage;
214212

213+
const isSyncedInStorage = isNonMediaFile && file?.path === 'storage';
214+
const isStillSyncing = isNonMediaFile && !file?.path;
215+
215216
const [downloadStarted, setDownloadStarted] = useState(false);
216217

218+
const handleSaveToDownloads = useCallback(() => {
219+
if (!file) return;
220+
const topic = pendingRemoteFile?.topic || '';
221+
Rooms.saveToDownloads(replyHash || file.hash || '', file.fileName || '', topic);
222+
Toast.show({ text1: 'Saving...', type: 'info' });
223+
}, [file, pendingRemoteFile, replyHash]);
224+
217225
const handleDownload = () => {
218226
setDownloadStarted(true);
219227
if (pendingRemoteFile) Rooms.download(pendingRemoteFile);
@@ -227,7 +235,7 @@ const MessageItemInner: React.FC<Props> = ({
227235

228236
useEffect(() => {
229237
const active =
230-
(isUndownloadedFile && !pendingRemoteFile) ||
238+
isStillSyncing ||
231239
(!!pendingRemoteFile &&
232240
!file?.path &&
233241
(downloadStarted || !!fileDl));
@@ -240,7 +248,7 @@ const MessageItemInner: React.FC<Props> = ({
240248
}, 200);
241249
return () => clearInterval(id);
242250
}, [
243-
isUndownloadedFile,
251+
isStillSyncing,
244252
pendingRemoteFile,
245253
fileDl?.hash,
246254
fileDl?.progress,
@@ -252,13 +260,8 @@ const MessageItemInner: React.FC<Props> = ({
252260
setWaveformError(false);
253261
}, [audioDetails?.audioPath]);
254262

255-
const onWaveformLoadState = useCallback((loading: boolean) => {
256-
setIsLoading(loading);
257-
}, []);
258-
259263
const onWaveformError = useCallback((error: any) => {
260264
console.log('Waveform error:', error);
261-
setIsLoading(false);
262265
setWaveformError(true);
263266
}, []);
264267

@@ -474,7 +477,7 @@ const MessageItemInner: React.FC<Props> = ({
474477
</TouchableOpacity>
475478
)}
476479

477-
{!isLoading && audioDetails?.isAudioMessage && (
480+
{audioDetails?.isAudioMessage && (
478481
<View style={styles.waveFormWrapper}>
479482
<Pressable
480483
onPress={handlePlayPauseAction}
@@ -523,10 +526,10 @@ const MessageItemInner: React.FC<Props> = ({
523526
<VideoPlayer path={videoDetails.videoPath} />
524527
)}
525528

526-
{isUndownloadedFile && !pendingRemoteFile && (
529+
{isStillSyncing && (
527530
<View style={styles.filePendingBox}>
528531
<TextField size="xsmall" type="muted">
529-
{t('syncingFileFromPeers', 'Waiting for peer…')}
532+
{t('syncingFileFromPeers', 'Syncing file…')}
530533
</TextField>
531534
<View style={styles.progressTrack}>
532535
<View
@@ -540,11 +543,26 @@ const MessageItemInner: React.FC<Props> = ({
540543
</View>
541544
)}
542545

546+
{isSyncedInStorage && (
547+
<TouchableOpacity
548+
onPress={handleSaveToDownloads}
549+
style={styles.downloadButton}>
550+
<CustomIcon
551+
type="FI"
552+
name="download"
553+
color={theme.primaryForeground}
554+
size={16}
555+
/>
556+
<TextField size="xsmall" style={styles.downloadText}>
557+
{t('saveToDownloads', 'Save')}{file?.fileName || message}
558+
</TextField>
559+
</TouchableOpacity>
560+
)}
561+
543562
{!audioDetails?.isAudioMessage &&
544563
!imageDetails?.isImageMessage &&
545564
!videoDetails.isVideoMessage &&
546-
!pendingRemoteFile &&
547-
!isUndownloadedFile &&
565+
!isNonMediaFile &&
548566
message && (
549567
<TextField
550568
size="small"
@@ -554,43 +572,6 @@ const MessageItemInner: React.FC<Props> = ({
554572
</TextField>
555573
)}
556574

557-
{pendingRemoteFile && !file?.path && (
558-
<>
559-
{downloadStarted || fileDl ? (
560-
<View style={styles.filePendingBox}>
561-
<TextField size="xsmall" type="muted">
562-
{t('downloading', 'Downloading…')}
563-
</TextField>
564-
<View style={styles.progressTrack}>
565-
<View
566-
style={[
567-
styles.progressFill,
568-
{ width: `${Math.min(visualProgress, 95)}%` },
569-
]}
570-
/>
571-
</View>
572-
<TextField size="xsmall">
573-
{pendingRemoteFile.fileName}
574-
</TextField>
575-
</View>
576-
) : (
577-
<TouchableOpacity
578-
onPress={handleDownload}
579-
style={styles.downloadButton}>
580-
<CustomIcon
581-
type="FI"
582-
name="download"
583-
color={theme.primaryForeground}
584-
size={16}
585-
/>
586-
<TextField size="xsmall" style={styles.downloadText}>
587-
{pendingRemoteFile.fileName}
588-
</TextField>
589-
</TouchableOpacity>
590-
)}
591-
</>
592-
)}
593-
594575
{huginLink && <GroupInvite invite={huginLink} />}
595576
{tip && <Tip tip={tip as TipType} />}
596577
<Reactions items={reactions} onReact={onPressReaction} />
@@ -620,9 +601,6 @@ const MessageItemInner: React.FC<Props> = ({
620601
export const MessageItem = React.memo(MessageItemInner);
621602

622603
const styles = StyleSheet.create({
623-
waveformSpinner: {
624-
marginRight: 8,
625-
},
626604
waveFormWrapper: {
627605
flex: 1,
628606
flexDirection: 'row',

src/lib/native.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ export class Swarm {
198198
rpc.send(data);
199199
}
200200

201+
saveToDownloads(hash, fileName, topic) {
202+
const data = { type: 'save_to_downloads', hash, fileName, topic };
203+
rpc.send(data);
204+
}
205+
201206
leave(key) {
202207
const data = { type: 'end_swarm', key };
203208
return rpc.send(data);

src/lib/rpc.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,11 @@ export class Bridge {
372372
WebRTC.signal(key, topic, address, data);
373373
break;
374374
case 'error-message':
375-
console.log('HUGE ERROR:', json.data)
375+
console.log('Error:', json.data);
376+
Toast.show({ text1: json.data?.message || 'Error', type: 'error' });
377+
break;
378+
case 'file-saved-to-downloads':
379+
Toast.show({ text1: `Saved ${json.data?.fileName || 'file'}`, type: 'success' });
376380
break;
377381
case 'downloading': {
378382
useGlobalStore.getState().patchFileDownload({

0 commit comments

Comments
 (0)