Skip to content

Commit dd9db06

Browse files
author
FileShot
committed
Desktop v1.4.8: FileShot Drive letter auto-upload
1 parent de0e520 commit dd9db06

2 files changed

Lines changed: 252 additions & 1 deletion

File tree

main.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ function getDriveUploadedDir() {
6464
return path.join(getDriveInboxDir(), '_uploaded');
6565
}
6666

67+
function getDriveFailedDir() {
68+
return path.join(getDriveInboxDir(), '_failed');
69+
}
70+
6771
function getConfiguredDriveLetter() {
6872
const raw = String(store.get('drive.letter', 'F') || 'F').trim();
6973
const letter = raw.replace(':', '').toUpperCase().slice(0, 1);
@@ -142,6 +146,141 @@ async function ensureDriveDirs() {
142146
fs.mkdirSync(getDriveInboxDir(), { recursive: true });
143147
fs.mkdirSync(getDriveUploadingDir(), { recursive: true });
144148
fs.mkdirSync(getDriveUploadedDir(), { recursive: true });
149+
fs.mkdirSync(getDriveFailedDir(), { recursive: true });
150+
}
151+
152+
// ============================================================================
153+
// STORAGE QUOTA (tier usage/limit)
154+
// ============================================================================
155+
156+
const STORAGE_USAGE_TTL_MS = 2 * 60 * 1000;
157+
let storageUsageCache = {
158+
data: null,
159+
lastFetchMs: 0,
160+
lastError: null
161+
};
162+
163+
function formatBytes(bytes) {
164+
const n = Number(bytes);
165+
if (!Number.isFinite(n) || n < 0) return '0 B';
166+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
167+
let v = n;
168+
let i = 0;
169+
while (v >= 1024 && i < units.length - 1) {
170+
v /= 1024;
171+
i++;
172+
}
173+
const digits = i === 0 ? 0 : i === 1 ? 1 : 2;
174+
return `${v.toFixed(digits)} ${units[i]}`;
175+
}
176+
177+
function formatQuotaLine(info) {
178+
if (!info) return 'Storage: (not loaded yet)';
179+
const tier = String(info.tier || 'free');
180+
const usage = Number(info.usage || 0);
181+
const limit = info.limit;
182+
if (limit === null || typeof limit === 'undefined') {
183+
return `Storage: ${formatBytes(usage)} used (Tier: ${tier})`;
184+
}
185+
const lim = Number(limit);
186+
if (!Number.isFinite(lim) || lim <= 0) {
187+
return `Storage: ${formatBytes(usage)} used (Tier: ${tier})`;
188+
}
189+
const remaining = Math.max(0, lim - usage);
190+
return `Storage: ${formatBytes(usage)} / ${formatBytes(lim)} (Remaining: ${formatBytes(remaining)})`;
191+
}
192+
193+
async function fetchStorageUsageFromApi() {
194+
const token = store.get('authToken', null);
195+
if (!token) throw new Error('Not logged in');
196+
197+
const res = await axios.get(`${API_URL}/files/usage`, {
198+
headers: { Authorization: `Bearer ${token}` },
199+
timeout: 15000
200+
});
201+
202+
const usage = res?.data?.usage;
203+
const limit = res?.data?.limit;
204+
const tier = res?.data?.tier;
205+
206+
if (typeof usage === 'undefined') throw new Error('Usage API returned no usage');
207+
return { usage, limit, tier };
208+
}
209+
210+
function buildStorageStatusText({ info, error }) {
211+
const now = new Date();
212+
const lines = [];
213+
lines.push('FileShot Drive — Storage Status');
214+
lines.push('');
215+
lines.push(`Updated: ${now.toLocaleString()}`);
216+
lines.push('');
217+
218+
if (error) {
219+
lines.push('Status: unavailable');
220+
lines.push(`Error: ${String(error)}`);
221+
} else if (info) {
222+
const tier = String(info.tier || 'free');
223+
const usage = Number(info.usage || 0);
224+
const limit = info.limit;
225+
lines.push(`Tier: ${tier}`);
226+
if (limit === null || typeof limit === 'undefined') {
227+
lines.push(`Used: ${formatBytes(usage)}`);
228+
lines.push('Limit: Unlimited');
229+
} else {
230+
const lim = Number(limit);
231+
lines.push(`Used: ${formatBytes(usage)}`);
232+
lines.push(`Limit: ${formatBytes(lim)}`);
233+
lines.push(`Remaining: ${formatBytes(Math.max(0, lim - usage))}`);
234+
}
235+
lines.push('');
236+
lines.push('Tip: Upgrade or manage storage at https://fileshot.io/pricing.html');
237+
} else {
238+
lines.push('Status: not loaded');
239+
lines.push('Log in to see your plan storage limit.');
240+
}
241+
242+
lines.push('');
243+
lines.push('Note: Windows Explorer drive “capacity” cannot be customized when using SUBST.');
244+
lines.push('This file shows your actual FileShot account storage limit.');
245+
lines.push('');
246+
return lines.join(os.EOL);
247+
}
248+
249+
async function writeDriveStorageStatusFile({ info, error }) {
250+
if (!isDriveFeatureAvailable()) return;
251+
try {
252+
await ensureDriveDirs();
253+
const statusPath = path.join(getDriveInboxDir(), 'FILESHOT_STORAGE.txt');
254+
fs.writeFileSync(statusPath, buildStorageStatusText({ info, error }), 'utf8');
255+
} catch (_) {
256+
// Best-effort.
257+
}
258+
}
259+
260+
async function refreshStorageUsage({ force = false, reason = '' } = {}) {
261+
const now = Date.now();
262+
const isFresh = storageUsageCache.data && (now - storageUsageCache.lastFetchMs) < STORAGE_USAGE_TTL_MS;
263+
if (!force && isFresh) return storageUsageCache.data;
264+
265+
try {
266+
const info = await fetchStorageUsageFromApi();
267+
storageUsageCache = { data: info, lastFetchMs: now, lastError: null };
268+
await writeDriveStorageStatusFile({ info, error: null });
269+
rebuildTrayMenu();
270+
return info;
271+
} catch (e) {
272+
const msg = e?.response?.data?.error || e?.message || String(e);
273+
storageUsageCache = { ...storageUsageCache, lastFetchMs: now, lastError: msg };
274+
await writeDriveStorageStatusFile({ info: storageUsageCache.data, error: msg });
275+
// Don't spam rebuilds on failure; but tray may need to show error.
276+
rebuildTrayMenu();
277+
return storageUsageCache.data;
278+
} finally {
279+
if (reason) {
280+
// Lightweight debug breadcrumb.
281+
try { console.log('[StorageUsage] refreshed', { reason, ok: !storageUsageCache.lastError }); } catch (_) {}
282+
}
283+
}
145284
}
146285

147286
function sanitizeFileName(name) {
@@ -382,6 +521,62 @@ async function startDriveWatcher() {
382521
const baseName = path.basename(originalPath);
383522
const safeBase = sanitizeFileName(baseName);
384523

524+
// Best-effort: check quota before uploading so we can fail fast with a clear message.
525+
// If we can't fetch usage (offline/not logged in), we still attempt the upload and let the server enforce.
526+
let quotaInfo = null;
527+
try {
528+
quotaInfo = await refreshStorageUsage({ force: false, reason: 'drive:precheck' });
529+
} catch (_) {
530+
quotaInfo = storageUsageCache.data;
531+
}
532+
533+
if (quotaInfo && quotaInfo.limit !== null && typeof quotaInfo.limit !== 'undefined') {
534+
const usage = Number(quotaInfo.usage || 0);
535+
const limit = Number(quotaInfo.limit);
536+
const fileSize = Number(st.size || 0);
537+
if (Number.isFinite(limit) && limit > 0 && Number.isFinite(usage) && (usage + fileSize) > limit) {
538+
const tier = String(quotaInfo.tier || 'free');
539+
const msg = `Storage full (${tier}): ${formatBytes(usage)} used of ${formatBytes(limit)}. File is ${formatBytes(fileSize)}.`;
540+
541+
try {
542+
notifier.notify({
543+
title: 'FileShot Drive',
544+
message: msg,
545+
icon: path.join(__dirname, 'assets', 'icon.png'),
546+
sound: false
547+
});
548+
} catch (_) {}
549+
550+
// Move into _failed so it's not retried endlessly.
551+
try {
552+
fs.mkdirSync(getDriveFailedDir(), { recursive: true });
553+
let dest = path.join(getDriveFailedDir(), safeBase);
554+
if (fs.existsSync(dest)) {
555+
const ext = path.extname(safeBase);
556+
const stem = safeBase.slice(0, safeBase.length - ext.length);
557+
dest = path.join(getDriveFailedDir(), `${stem}-${Date.now()}${ext}`);
558+
}
559+
fs.renameSync(originalPath, dest);
560+
561+
const errPath = `${dest}.error.txt`;
562+
const details = [
563+
'FileShot Drive — Upload blocked (storage limit reached)',
564+
'',
565+
msg,
566+
'',
567+
'Manage your storage or upgrade:',
568+
'https://fileshot.io/pricing.html',
569+
''
570+
].join(os.EOL);
571+
fs.writeFileSync(errPath, details, 'utf8');
572+
} catch (_) {
573+
// If we can't move it, leave it where it is.
574+
}
575+
576+
continue;
577+
}
578+
}
579+
385580
// Move into _uploading to prevent re-trigger and to visually separate state.
386581
const uploadingPath = path.join(getDriveUploadingDir(), safeBase);
387582
let workPath = originalPath;
@@ -576,6 +771,11 @@ async function enableFileShotDrive() {
576771
await startDriveWatcher();
577772
rebuildTrayMenu();
578773

774+
// Best-effort: create/update the storage status file immediately.
775+
refreshStorageUsage({ force: true, reason: 'drive:enabled' }).catch(() => {
776+
writeDriveStorageStatusFile({ info: storageUsageCache.data, error: storageUsageCache.lastError }).catch(() => {});
777+
});
778+
579779
try {
580780
shell.openPath(getDriveRootPath(letter));
581781
} catch (_) {}
@@ -1030,6 +1230,12 @@ function buildTrayMenuTemplate() {
10301230
const driveEnabled = Boolean(store.get('drive.enabled', false));
10311231
const driveLetter = getConfiguredDriveLetter();
10321232

1233+
const storageInfo = storageUsageCache.data;
1234+
const storageError = storageUsageCache.lastError;
1235+
const storageLabel = storageError
1236+
? `Storage: (error) ${String(storageError).slice(0, 120)}`
1237+
: formatQuotaLine(storageInfo);
1238+
10331239
return [
10341240
{
10351241
label: 'Open FileShot',
@@ -1066,6 +1272,16 @@ function buildTrayMenuTemplate() {
10661272
}
10671273
}
10681274
},
1275+
{
1276+
label: storageLabel,
1277+
enabled: false
1278+
},
1279+
{
1280+
label: 'Refresh Storage Info',
1281+
click: () => {
1282+
refreshStorageUsage({ force: true, reason: 'tray:refresh' }).catch(() => {});
1283+
}
1284+
},
10691285
{
10701286
label: 'Upload File',
10711287
click: () => {
@@ -1108,6 +1324,24 @@ function buildTrayMenuTemplate() {
11081324
{
11091325
label: 'Note: drop files to auto-upload',
11101326
enabled: false
1327+
},
1328+
{ type: 'separator' },
1329+
{
1330+
label: storageLabel,
1331+
enabled: false
1332+
},
1333+
{
1334+
label: 'Open Storage Status File',
1335+
click: () => {
1336+
ensureDriveDirs().catch(() => {});
1337+
shell.openPath(path.join(getDriveInboxDir(), 'FILESHOT_STORAGE.txt'));
1338+
}
1339+
},
1340+
{
1341+
label: 'Refresh Storage Info',
1342+
click: () => {
1343+
refreshStorageUsage({ force: true, reason: 'tray:drive-refresh' }).catch(() => {});
1344+
}
11111345
}
11121346
]
11131347
},
@@ -1363,10 +1597,14 @@ ipcMain.handle('get-auth-token', () => {
13631597

13641598
ipcMain.handle('set-auth-token', (event, token) => {
13651599
store.set('authToken', token);
1600+
refreshStorageUsage({ force: true, reason: 'auth:set-token' }).catch(() => {});
13661601
});
13671602

13681603
ipcMain.handle('clear-auth-token', () => {
13691604
store.delete('authToken');
1605+
storageUsageCache = { data: null, lastFetchMs: 0, lastError: null };
1606+
writeDriveStorageStatusFile({ info: null, error: null }).catch(() => {});
1607+
rebuildTrayMenu();
13701608
});
13711609

13721610
ipcMain.handle('add-recent-upload', (event, upload) => {
@@ -2004,6 +2242,19 @@ app.whenReady().then(() => {
20042242
createWindow();
20052243
createTray();
20062244

2245+
// Keep storage info reasonably fresh (tray + FILESHOT_STORAGE.txt) while logged in.
2246+
if (store.get('authToken', null)) {
2247+
refreshStorageUsage({ force: true, reason: 'startup' }).catch(() => {});
2248+
} else {
2249+
// Ensure the status file exists with helpful guidance when the drive is enabled.
2250+
writeDriveStorageStatusFile({ info: null, error: null }).catch(() => {});
2251+
}
2252+
2253+
setInterval(() => {
2254+
if (!store.get('authToken', null)) return;
2255+
refreshStorageUsage({ force: true, reason: 'poll' }).catch(() => {});
2256+
}, 5 * 60 * 1000);
2257+
20072258
// Windows: enable FileShot Drive by default on first run.
20082259
// (This was the core point of the v1.4.8 update; requiring a manual tray toggle is too easy to miss.)
20092260
if (isDriveFeatureAvailable()) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fileshot-desktop",
3-
"version": "1.4.9",
3+
"version": "1.4.10",
44
"description": "FileShot.io Desktop Application - Fast, Private File Sharing",
55
"main": "main.js",
66
"scripts": {

0 commit comments

Comments
 (0)