LCOV - code coverage report
Current view: top level - pkg/lib - cockpit-upload-helper.ts Coverage Total Hit
Test: cockpit Lines: 80.0 % 125 100
Test Date: 2026-07-03 07:31:16

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2024 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6            5 : import cockpit from 'cockpit';
       7              : 
       8              : // These are the same values used by the bridge (in channel.py)
       9            5 : const BLOCK_SIZE = 16 << 10; // 16kiB
      10            5 : const FLOW_WINDOW = 2 << 20; // 2MiB
      11              : 
      12            2 : function debug(...args: unknown[]) {
      13            0 :     if (window.debugging == 'all' || window.debugging?.includes('upload'))
      14            0 :         console.debug('upload', ...args);
      15            2 : }
      16              : 
      17            1 : class UploadError extends Error {
      18            1 :     name = 'UploadError';
      19            5 : }
      20              : 
      21            2 : class Waiter {
      22            1 :     unblock = () => { };
      23            2 :     block(): Promise<void> {
      24            2 :         return new Promise(resolve => { this.unblock = resolve });
      25            2 :     }
      26            5 : }
      27              : 
      28            2 : export async function upload(
      29            2 :     destination: string,
      30            2 :     contents: Blob,
      31            2 :     progress?: (bytes_sent: number) => void,
      32            2 :     signal?: AbortSignal,
      33            2 :     options?: cockpit.JsonObject
      34            2 : ) {
      35            2 :     let close_message = null as (cockpit.JsonObject | null);
      36            2 :     let outstanding = 0; // for flow control
      37            2 :     let delivered = 0; // for progress reporting
      38              : 
      39              :     // This variable is the most important thing in this function.  The main
      40              :     // upload loop will do work for as long as it can, and then it .block()s on
      41              :     // the waiter until something changes (ack, close, abort, etc).  All of
      42              :     // those things call .unblock() to resume the loop.
      43            2 :     const event_waiter = new Waiter();
      44              : 
      45            2 :     if (signal) {
      46            2 :         signal.throwIfAborted(); // early exit
      47            2 :         signal.addEventListener('abort', event_waiter.unblock);
      48            2 :     }
      49              : 
      50            2 :     const opts = {
      51            2 :         payload: 'fsreplace1',
      52            2 :         path: destination,
      53            2 :         binary: true,
      54            2 :         size: contents.size,
      55            2 :         'send-acks': 'bytes',
      56            2 :         ...options,
      57            2 :     } as const;
      58            2 :     debug('requesting channel', opts);
      59            2 :     const channel = cockpit.channel(opts);
      60            2 :     channel.addEventListener('control', (_ev, message) => {
      61            2 :         debug('control', message);
      62            2 :         if (message.command === 'ack') {
      63            2 :             cockpit.assert(typeof message.bytes === 'number', 'bytes not a number');
      64            2 :             delivered += message.bytes;
      65            2 :             if (progress) {
      66            2 :                 debug('progress', delivered);
      67            2 :                 progress(delivered);
      68            2 :             }
      69            2 :             outstanding -= message.bytes;
      70            2 :             debug('outstanding -- to', outstanding);
      71            2 :             event_waiter.unblock();
      72            2 :         }
      73            2 :     });
      74            2 :     channel.addEventListener('close', (_ev, message) => {
      75            2 :         debug('close', message);
      76            2 :         close_message = message;
      77            2 :         event_waiter.unblock();
      78            2 :     });
      79              : 
      80            2 :     try {
      81            2 :         debug('starting file send', contents);
      82              : 
      83              :         /* We want to use the "bring your own buffer" (byob) API so that we can
      84              :          * decide the size of the blocks to read from the file: this is needed
      85              :          * for flow control reasons and also to respect internal limitations in
      86              :          * cockpit-ws.  "byob" is not available on WebKit, though:
      87              :          *
      88              :          *    https://caniuse.com/mdn-api_readablestreambyobreader
      89              :          *
      90              :          * Check if the API is available, and fake it if not.
      91              :          */
      92            2 :         let read;
      93            2 :         if (typeof ReadableStreamBYOBReader === 'function') {
      94            2 :             const reader = contents.stream().getReader({ mode: 'byob' });
      95            2 :             read = () => reader.read(new Uint8Array(BLOCK_SIZE));
      96            0 :         } else {
      97              :             // fallback code (no 'byob' available)
      98            0 :             const reader = contents.stream().getReader();
      99            0 :             let buffer: Uint8Array | null = null;
     100            0 :             read = async () => {
     101              :                 // No buffered data?  Try a read.
     102            0 :                 if (!buffer) {
     103            0 :                     const { done, value } = await reader.read();
     104            0 :                     if (done) {
     105            0 :                         return { done, value };
     106            0 :                     } else {
     107            0 :                         buffer = value;
     108            0 :                     }
     109            0 :                 }
     110              : 
     111              :                 // Return the buffered data: if length < size, return it all, else return a slice
     112            0 :                 if (buffer.length < BLOCK_SIZE) {
     113            0 :                     const value = buffer;
     114            0 :                     buffer = null;
     115            0 :                     return { done: false, value };
     116            0 :                 } else {
     117            0 :                     const value = buffer.slice(0, BLOCK_SIZE);
     118            0 :                     buffer = buffer.slice(BLOCK_SIZE);
     119            0 :                     return { done: false, value };
     120            0 :                 }
     121            0 :             };
     122            0 :         }
     123              : 
     124            2 :         let eof = false;
     125              : 
     126              :         // eslint-disable-next-line no-unmodified-loop-condition
     127            2 :         while (!close_message) {
     128              :             /* We do the following steps for as long as the channel is open:
     129              :              *  - if there is room to write more data, do that
     130              :              *  - otherwise, block on the waiter until something changes
     131              :              *  - in any case, check for cancellation, repeat
     132              :              * The idea here is that each loop iteration will `await` one
     133              :              * thing, and once it returns, we need to re-evaluate our state.
     134              :              */
     135            2 :             if (!eof && outstanding < FLOW_WINDOW) {
     136            2 :                 const { done, value } = await read();
     137            2 :                 if (done) {
     138            2 :                     debug('sending done');
     139            2 :                     channel.control({ command: 'done' });
     140            2 :                     eof = true;
     141            2 :                 } else {
     142            2 :                     debug('sending', value.length, 'bytes');
     143            2 :                     channel.send(value);
     144            2 :                     outstanding += value.length;
     145            2 :                     debug('outstanding ++ to', outstanding);
     146            2 :                 }
     147            2 :                 if (signal) {
     148            2 :                     signal.throwIfAborted();
     149            2 :                 }
     150            2 :             } else {
     151            2 :                 debug('sleeping', outstanding, 'of', FLOW_WINDOW, 'eof', eof);
     152            2 :                 await event_waiter.block();
     153            2 :             }
     154            2 :             if (signal) {
     155            2 :                 signal.throwIfAborted();
     156            2 :             }
     157            2 :         }
     158              : 
     159            1 :         if (close_message.problem) {
     160            1 :             throw new UploadError(cockpit.message(close_message));
     161            1 :         } else {
     162            2 :             cockpit.assert(typeof close_message.tag === 'string', "tag missing on close message");
     163            2 :             return close_message.tag;
     164            2 :         }
     165            2 :     } finally {
     166            2 :         debug('finally');
     167            2 :         channel.close(); // maybe we got aborted
     168            2 :     }
     169            2 : }
        

Generated by: LCOV version 2.0-1