Repository: hydralauncher/hydra
File: src/main/services/download/js-http-downloader.ts (TypeScript)

Code before:
        actualFilePath = path.join(savePath, headerFilename);
        this.folderName = headerFilename;
        logger.log(
          `[JsHttpDownloader] Using filename from Content-Disposition: ${headerFilename}`
        );
      }
    }

    if (!response.body) {
      throw new Error("Response body is null");
    }

    const flags = startByte > 0 ? "a" : "w";
    this.writeStream = fs.createWriteStream(actualFilePath, { flags });

    const readableStream = this.createReadableStream(response.body.getReader());
    await pipeline(readableStream, this.writeStream);

    this.status = "complete";
    this.retryCount = 0;
    this.downloadSpeed = 0;
    logger.log("[JsHttpDownloader] Download complete");
  }

  private parseContentDisposition(response: Response): string | undefined {
    const header = response.headers.get("content-disposition");
    if (!header) return undefined;

    // Try to extract filename from Content-Disposition header
    // Formats: attachment; filename="file.zip" or attachment; filename=file.zip
    const filenameMatch = /filename\*?=['"]?(?:UTF-8'')?([^"';\n]+)['"]?/i.exec(
      header
    );
    if (filenameMatch?.[1]) {
      try {
        return decodeURIComponent(filenameMatch[1].trim());
      } catch {
        return filenameMatch[1].trim();
      }
    }
    return undefined;
  }

  private createReadableStream(
    reader: ReadableStreamDefaultReader<Uint8Array>
  ): Readable {
    const onChunk = (length: number) => {
      this.bytesDownloaded += length;
      this.lastDataReceivedAt = Date.now();
      this.updateSpeed();
    };

Review comment (bug):
after setting `this.status = "complete"`, the `finally` block in `startDownloadWithRetry()` still runs and currently calls `cleanup()`. If cleanup forces `status` back to paused, completion state will be wrong. worth adding a quick unit/integration check for “download completes -> status stays complete”.

Code after:

    this.parseFileSize(response, startByte);

    let actualFilePath = filePath;
    if (usedFallback && startByte === 0) {
      const headerFilename = this.parseContentDisposition(response);
      if (headerFilename) {
        actualFilePath = path.join(savePath, headerFilename);
        this.folderName = headerFilename;
        logger.log(
          `[JsHttpDownloader] Using filename from Content-Disposition: ${headerFilename}`
        );
      }
    }

    if (!response.body) {
      throw new Error("Response body is null");
    }

    const flags = startByte > 0 ? "a" : "w";
    this.writeStream = fs.createWriteStream(actualFilePath, { flags });

    const readableStream = this.createReadableStream(response.body.getReader());
    await pipeline(readableStream, this.writeStream);

    this.status = "complete";
    this.retryCount = 0;
    this.downloadSpeed = 0;
    logger.log("[JsHttpDownloader] Download complete");
  }

  private parseContentDisposition(response: Response): string | undefined {
    const header = response.headers.get("content-disposition");
    if (!header) return undefined;

    const filenameMatch = /filename\*?=['"]?(?:UTF-8'')?([^"';\n]+)['"]?/i.exec(
      header
    );
    if (filenameMatch?.[1]) {
      try {
        return decodeURIComponent(filenameMatch[1].trim());
      } catch {
        return filenameMatch[1].trim();
      }
    }
    return undefined;
  }

  private createReadableStream(
    reader: ReadableStreamDefaultReader<Uint8Array>
  ): Readable {