Skip to content

feat: download and share buttons + handling that#20

Merged
yamijuan merged 3 commits intomainfrom
nikita-download-and-share-buttons
May 14, 2025
Merged

feat: download and share buttons + handling that#20
yamijuan merged 3 commits intomainfrom
nikita-download-and-share-buttons

Conversation

@nikitalokhmachev-ai
Copy link
Collaborator

@nikitalokhmachev-ai nikitalokhmachev-ai commented May 13, 2025

User description

This PR addresses #18.

It adds two new buttons to the UI:

  1. Download button – Allows users to select archived pages and download them as a WACZ file.
  2. Share button – The user selects pages to seed via WebTorrent and clicks the share button. The selected pages are saved to OPFS and seeded using a generated magnet link.

We will need to work more on the UX aspect of this but the core functionality is there.


PR Type

Enhancement


Description

  • Added download functionality for WACZ archives

  • Implemented WebTorrent sharing capability

  • Added UI controls for selecting pages

  • Integrated with OPFS for file storage


Changes walkthrough 📝

Relevant files
Enhancement
3 files
sidepanel.ts
Implement download and share functionality                             
+142/-2 
argo-archive-list.ts
Add page selection functionality                                                 
+44/-8   
sidepanel.html
Add WebTorrent script and archive list ID                               
+2/-1     
Configuration changes
4 files
webtorrent-browser.d.ts
Add WebTorrent module type definitions                                     
+22/-0   
webtorrent-global.d.ts
Add WebTorrent global type definitions                                     
+20/-0   
webpack.config.js
Configure WebTorrent dependencies                                               
+7/-0     
tsconfig.json
Update TypeScript configuration                                                   
+2/-0     
Dependencies
2 files
package.json
Add WebTorrent and related dependencies                                   
+7/-4     
webtorrent.min.js
Add WebTorrent client library                                                       
+16/-0   

Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • @nikitalokhmachev-ai
    Copy link
    Collaborator Author

    image

    @pr-agent-monadical
    Copy link

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    🎫 Ticket compliance analysis 🔶

    18 - Partially compliant

    Compliant requirements:

    • Implement P2P sharing of web archives using WebTorrent
    • Generate WACZ files from IndexedDB content for sharing
    • Store WACZ files in a browser-accessible location (OPFS)
    • Seed WACZ files using WebTorrent

    Non-compliant requirements:

    • Create links to replay viewer that can load content from torrents with magnet links
    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Code Duplication

    The onDownload and onShare methods contain significant duplicated code for generating and downloading WACZ files. This could be refactored into a shared utility function to improve maintainability.

    private async onDownload() {
      const selectedPages = this.archiveList?.getSelectedPages?.() || [];
      if (!selectedPages.length) {
        alert("Please select some pages to share.");
        return;
      }
    
      console.log("Selected pages to share:", selectedPages);
    
      const defaultCollId = (await getLocalOption("defaultCollId")) || "";
      const coll = await collLoader.loadColl(defaultCollId);
    
      const pageTsList = selectedPages.map((p) => p.id);
      const format = "wacz";
      const filename = `archive-${Date.now()}.wacz`;
    
      // Webrecorder swlib API format for download:
      const downloader = new Downloader({
        coll,
        format,
        filename,
        pageList: pageTsList,
      });
    
      const response = await downloader.download();
      if (!(response instanceof Response)) {
        console.error("Download failed:", response);
        alert("Failed to download archive.");
        return;
      }
    
      console.log("Download response:", response);
    
      const blob = await response.blob();
      const url = URL.createObjectURL(blob);
    
      // Create temporary <a> to trigger download
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
    
      // Cleanup
      URL.revokeObjectURL(url);
      document.body.removeChild(a);
    
      console.log("WACZ file downloaded:", filename);
    }
    
    private async onShare() {
      const selectedPages = this.archiveList?.getSelectedPages?.() || [];
      if (!selectedPages.length) {
        alert("Please select some pages to share.");
        return;
      }
    
      console.log("Selected pages to share:", selectedPages);
    
      const defaultCollId = (await getLocalOption("defaultCollId")) || "";
      const coll = await collLoader.loadColl(defaultCollId);
    
      const pageTsList = selectedPages.map((p) => p.id);
      const format = "wacz";
      const filename = `archive-${Date.now()}.wacz`;
    
      // Webrecorder swlib API format for download:
      const downloader = new Downloader({
        coll,
        format,
        filename,
        pageList: pageTsList,
      });
    
      const response = await downloader.download();
      if (!(response instanceof Response)) {
        console.error("Download failed:", response);
        alert("Failed to download archive.");
        return;
      }
    
      const opfsRoot = await navigator.storage.getDirectory();
      const waczFileHandle = await opfsRoot.getFileHandle(filename, {
        create: true,
      });
      const writable = await waczFileHandle.createWritable();
    
      const reader = response.body!.getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        await writable.write(value);
      }
    
      await writable.close();
    
      console.log("WACZ saved to OPFS as:", filename);
    
      // Get a File object from OPFS
      const fileHandle = await opfsRoot.getFileHandle(filename);
      const file = await fileHandle.getFile();
    
      // Create a WebTorrent client if not already available
      const client = new (window as any).WebTorrent();
    
      // Seed the file
      // @ts-expect-error
      client.seed(file, (torrent) => {
        const magnetURI = torrent.magnetURI;
        console.log("Seeding WACZ file via WebTorrent:", magnetURI);
    
        // Optionally display/share this magnet link
        alert(`Magnet Link Ready:\n${magnetURI}`);
      });
    }
    Resource Cleanup

    The WebTorrent client is created but there's no code to handle cleanup when the user navigates away or closes the panel. This could lead to memory leaks or continued seeding when no longer needed.

    const client = new (window as any).WebTorrent();
    
    // Seed the file
    // @ts-expect-error
    client.seed(file, (torrent) => {
      const magnetURI = torrent.magnetURI;
      console.log("Seeding WACZ file via WebTorrent:", magnetURI);
    
      // Optionally display/share this magnet link
      alert(`Magnet Link Ready:\n${magnetURI}`);
    });
    Error Handling

    The WebTorrent seeding process lacks comprehensive error handling. If seeding fails, there's no feedback to the user beyond console logs.

    // @ts-expect-error
    client.seed(file, (torrent) => {
      const magnetURI = torrent.magnetURI;
      console.log("Seeding WACZ file via WebTorrent:", magnetURI);
    
      // Optionally display/share this magnet link
      alert(`Magnet Link Ready:\n${magnetURI}`);
    });

    @nikitalokhmachev-ai
    Copy link
    Collaborator Author

    image
    The magnet link can be opened, using replayweb.page.

    Comment on lines 213 to 224
    const client = new (window as any).WebTorrent();

    // Seed the file
    // @ts-expect-error
    client.seed(file, (torrent) => {
    const magnetURI = torrent.magnetURI;
    console.log("Seeding WACZ file via WebTorrent:", magnetURI);

    // Optionally display/share this magnet link
    alert(`Magnet Link Ready:\n${magnetURI}`);
    });
    }

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Suggestion: The WebTorrent client is created but never destroyed, which can lead to memory leaks and continued seeding even after the user navigates away. Store the client instance as a class property and implement proper cleanup when the component is disconnected or when sharing is complete. [possible issue, importance: 8]

    Suggested change
    const client = new (window as any).WebTorrent();
    // Seed the file
    // @ts-expect-error
    client.seed(file, (torrent) => {
    const magnetURI = torrent.magnetURI;
    console.log("Seeding WACZ file via WebTorrent:", magnetURI);
    // Optionally display/share this magnet link
    alert(`Magnet Link Ready:\n${magnetURI}`);
    });
    }
    // Create a WebTorrent client if not already available
    const client = new (window as any).WebTorrent();
    this.torrentClient = client;
    // Seed the file
    // @ts-expect-error
    client.seed(file, (torrent) => {
    const magnetURI = torrent.magnetURI;
    console.log("Seeding WACZ file via WebTorrent:", magnetURI);
    // Optionally display/share this magnet link
    alert(`Magnet Link Ready:\n${magnetURI}`);
    });

    Comment on lines +184 to +204
    const response = await downloader.download();
    if (!(response instanceof Response)) {
    console.error("Download failed:", response);
    alert("Failed to download archive.");
    return;
    }

    const opfsRoot = await navigator.storage.getDirectory();
    const waczFileHandle = await opfsRoot.getFileHandle(filename, {
    create: true,
    });
    const writable = await waczFileHandle.createWritable();

    const reader = response.body!.getReader();
    while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    await writable.write(value);
    }

    await writable.close();

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Suggestion: The code doesn't handle errors during file writing operations. If any error occurs during reading from the response or writing to OPFS, the writable stream might remain open, causing resource leaks. Add proper error handling with try/catch and ensure the writable is closed even if an error occurs. [possible issue, importance: 7]

    Suggested change
    const response = await downloader.download();
    if (!(response instanceof Response)) {
    console.error("Download failed:", response);
    alert("Failed to download archive.");
    return;
    }
    const opfsRoot = await navigator.storage.getDirectory();
    const waczFileHandle = await opfsRoot.getFileHandle(filename, {
    create: true,
    });
    const writable = await waczFileHandle.createWritable();
    const reader = response.body!.getReader();
    while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    await writable.write(value);
    }
    await writable.close();
    const response = await downloader.download();
    if (!(response instanceof Response)) {
    console.error("Download failed:", response);
    alert("Failed to download archive.");
    return;
    }
    const opfsRoot = await navigator.storage.getDirectory();
    const waczFileHandle = await opfsRoot.getFileHandle(filename, {
    create: true,
    });
    const writable = await waczFileHandle.createWritable();
    try {
    const reader = response.body!.getReader();
    while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    await writable.write(value);
    }
    } catch (error) {
    console.error("Error writing file to OPFS:", error);
    alert("Failed to save archive.");
    await writable.close();
    return;
    }
    await writable.close();

    @nikitalokhmachev-ai
    Copy link
    Collaborator Author

    The magnet link is now copied to the user's clipboard automatically

    @Shrinks99
    Copy link
    Collaborator

    Looks great! Maybe later we could display the magnet link copied as a toast or disappearing tooltip?

    @yamijuan yamijuan merged commit af1ae7c into main May 14, 2025
    1 of 4 checks passed
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    3 participants