Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ export function registerDownloadDataset() {
throw new Error('No download path set')
}

const filePath = join(downloadPath, fileName)
const filePath = join(downloadPath, fileName);

// Check if file already exists
if (fs.existsSync(filePath)) {
throw new Error(`Creating torrent stream ${fileName}: File already exists`);
}
const fileHandle = await fs.open(filePath, 'w')

// Stocker le handle pour les écritures futures
Expand Down
11 changes: 11 additions & 0 deletions src/lib/electron-app/factories/ipcs/register-download-dummy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ipcMain } from "electron";
import { downloadService, DownloadProgress } from "main/services/download-dummy.service";
import { Asset } from "shared/api";

export function registerDownloadDummy() {
ipcMain.handle("download-file", (event, url: string, downloadPath: string, fileName: string, defaultFileNamePrefix: string) => {
Expand All @@ -11,4 +12,14 @@ export function registerDownloadDummy() {
(progress: DownloadProgress) => event.sender.send('download-progress', progress.progress, progress.speed, progress.eta)
);
});

ipcMain.handle('asset:download', (event, asset: Asset, downloadPath: string, fileName: string, defaultFileNamePrefix: string) => {
return downloadService.downloadAsset(
asset,
downloadPath,
fileName,
defaultFileNamePrefix,
(progress: DownloadProgress) => event.sender.send('download-progress', progress.progress, progress.speed, progress.eta)
);
})
}
116 changes: 104 additions & 12 deletions src/main/services/download-dummy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import * as fs from "fs";
import * as path from "path";
import { downloadStoreService } from "./download-store.service";
import { BandwidthLimiterService } from "./bandwidth-limiter.service";
import { KILO_BYTES, MEGA_BYTES } from "../../lib/electron-app/utils/units"
import { KILO_BYTES, MEGA_BYTES, GIGA_BYTES } from "../../lib/electron-app/utils/units"
import { FILE_REJECTION_CODES } from "shared/constants";
import { Asset } from 'shared/api'
import { rescueApiService } from "./rescue-api-service";
import { loggerService } from "./logger";

export interface DownloadProgress {
progress: number;
Expand All @@ -12,11 +16,15 @@ export interface DownloadProgress {

interface DownloadResult {
success: boolean;
statusCode?: number;
filePath?: string;
fileSize?: number;
error?: string;
}

const MAX_SEEDING_SIZE = GIGA_BYTES;
const MVP_BETA_LIMIT_SIZE = 400 * MEGA_BYTES;

class DownloadService {
private bandwidthLimiterService: BandwidthLimiterService;

Expand All @@ -40,6 +48,7 @@ class DownloadService {
onProgress?: (progress: DownloadProgress) => void
): Promise<DownloadResult> {
let filePath: string | undefined;
let statusCode: number | undefined;

try {
if (!downloadPath) {
Expand All @@ -55,6 +64,7 @@ class DownloadService {

// Perform HTTP request
const response = await fetch(url);
statusCode = response.status;

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
Expand All @@ -70,30 +80,31 @@ class DownloadService {
const fileNameMatch = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;\r\n]+)/);
if (fileNameMatch) {
guessedFileName = fileNameMatch[1];
console.log(`Guessed filename from Content-Disposition: ${guessedFileName}`);
}
}

if (!guessedFileName) {
// Retrieving filename from URL, removing query parameters if any
guessedFileName = url.split("/").pop()?.split("?")[0];
console.log(`Guessed filename from URL: ${guessedFileName}`);
}

if (guessedFileName) {
// Checking if file already exists
const guessedFilePath = path.join(downloadPath, guessedFileName);
if (!fs.existsSync(guessedFilePath)) {
fileName = guessedFileName;
} else if (defaultFileNamePrefix) {
fileName = `${defaultFileNamePrefix}_${guessedFileName}`;
}
}

if (!fileName) {
fileName = `${Date.now()}`; // Default filename

if (defaultFileNamePrefix) {
}

// Always Prepend default prefix if provided
if (defaultFileNamePrefix) {
fileName = `${defaultFileNamePrefix}_${fileName}`;
}
}
}

Expand All @@ -108,6 +119,17 @@ class DownloadService {
10
);

// Abort download if Content-Length exceeds MVP beta limit size
if (totalSize > MVP_BETA_LIMIT_SIZE) {
loggerService.error(`File too big for seeding (Content-Length: ${totalSize} bytes)`);
return {
success: false,
fileSize: totalSize,
filePath: filePath,
error: "file too big (content-length)",
}
}

// Create read stream
const reader = response.body?.getReader();
if (!reader) {
Expand Down Expand Up @@ -135,6 +157,19 @@ class DownloadService {
writer.write(value);
downloadedSize += value.length;

// Abort download if it exceeds MVP beta limit size
if (downloadedSize > MVP_BETA_LIMIT_SIZE) {
loggerService.error(`File too big for seeding (Downloaded size so far : ${downloadedSize} bytes)`);
writer.end();

return {
success: false,
fileSize: downloadedSize,
filePath: filePath,
error: "file too big (downloaded size)",
};
}

// Calculate and report progress
if (onProgress) {
const progress = Math.round((downloadedSize / totalSize) * 100);
Expand Down Expand Up @@ -165,12 +200,10 @@ class DownloadService {
});
});

// Add file to downloaded files store
downloadStoreService.addDownloadedFile(filePath);

let result: DownloadResult = {
success: true,
filePath: filePath
filePath: filePath,
statusCode: statusCode
};

// We do not trust totalSize, fetching the file size from the file
Expand Down Expand Up @@ -199,10 +232,68 @@ class DownloadService {

return {
success: false,
error: errorMessage
error: errorMessage,
statusCode: statusCode
};
}
}

async downloadAsset(
asset: Asset,
downloadPath: string,
fileName?: string,
defaultFileNamePrefix?: string,
onProgress?: (progress: DownloadProgress) => void
): Promise<DownloadResult> {
const downloadResult = await this.downloadFile(
asset.url,
downloadPath,
fileName,
defaultFileNamePrefix,
onProgress
);

if (downloadResult) {
if (downloadResult.fileSize && downloadResult.fileSize > MVP_BETA_LIMIT_SIZE) {
// File is too big for seeding, call the API
rescueApiService.rejectAsset(
asset.res_id,
FILE_REJECTION_CODES.FILE_NOT_SUITABLE_FOR_BETA, // File too big for MVP beta
downloadResult.fileSize
);

if (downloadResult.filePath && fs.existsSync(downloadResult.filePath)) {
fs.unlinkSync(downloadResult.filePath);
}
} else if (downloadResult.success == true && downloadResult.filePath) {
// Add file to downloaded files store
downloadStoreService.addDownloadedFile(downloadResult.filePath);
} else if (downloadResult.statusCode === 404) {
loggerService.error(`Asset not found (404): Reporting to Rescue API.`);
// Link is broken, report to Rescue API
rescueApiService.rejectAsset(
asset.res_id,
FILE_REJECTION_CODES.HTTP_404, // Not found
);
} else if (downloadResult.statusCode === 403) {
loggerService.error(`Access denied (403): Reporting to Rescue API.`);
// Access denied, report to Rescue API
rescueApiService.rejectAsset(
asset.res_id,
FILE_REJECTION_CODES.HTTP_403, // Forbidden
);
} else if (downloadResult.error === "fetch failed") {
loggerService.error(`Network error during download: Reporting to Rescue API.`);
// Network error, report to Rescue API
rescueApiService.rejectAsset(
asset.res_id,
FILE_REJECTION_CODES.FETCH_FAILED, // Network error
);
}
}

return downloadResult;
}
}

export let downloadService = new DownloadService();
Expand All @@ -213,7 +304,7 @@ export let downloadService = new DownloadService();
* @param downloadPath - Destination directory
* @param onProgress - Callback to track overall progress
* @returns Promise<DownloadResult[]>
*/
* /
export async function downloadMultipleFiles(
downloads: Array<{ url: string }>,
downloadPath: string,
Expand Down Expand Up @@ -247,6 +338,7 @@ export async function downloadMultipleFiles(

return Promise.all(downloadPromises);
}
*/

/**
* Formats speed in MB/s or KB/s
Expand Down
2 changes: 0 additions & 2 deletions src/main/services/download-store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ class DownloadStoreService {
if (this.window) {
this.window.webContents.send('free-space:exhausted');
}

/** @todo Stop download process */
} else {
this.setRemainingFreeSpace(freeSpace);
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/services/rescue-api-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { config } from "config";
import { RESCUER_ID } from "shared/constants";

export interface ErrorHandlingOptions {
retry: number;
Expand Down Expand Up @@ -87,6 +88,21 @@ class RescueApiService {
}
}

async rejectAsset(resource_id: number, reason: number, size?: number): Promise<CallResult> {
return this.call('/asset/reject', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
rescuer_id: RESCUER_ID,
code: reason,
res_id: resource_id,
size: size
}),
});
}

private async handleRetry(route: string, options: any, fallbackResult: CallResult, errorHandlingOptions?: ErrorHandlingOptions): Promise<CallResult> {
// If caller requests a retry
if (errorHandlingOptions && errorHandlingOptions.retry > 0) {
Expand Down
6 changes: 5 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { contextBridge, ipcRenderer } from 'electron'
import { ErrorHandlingOptions } from 'main/services/rescue-api-service';
import { Asset } from 'shared/api';

declare global {
interface Window {
Expand Down Expand Up @@ -68,7 +69,10 @@ const API = {
// Dummy downloader
downloadFile: (url: string, downloadPath: string, filename?: string, defaultFileNamePrefix?: string) =>
ipcRenderer.invoke('download-file', url, downloadPath, filename, defaultFileNamePrefix),


downloadAsset: (asset: Asset, downloadPath: string, filename?: string, defaultFileNamePrefix?: string) =>
ipcRenderer.invoke('asset:download', asset, downloadPath, filename, defaultFileNamePrefix),

// Obtenir l'espace disque disponible (libre) pour un chemin donné
getFreeSpace: (path: string) => ipcRenderer.invoke('get-free-space', path),

Expand Down
Loading