archiverjs / node-archiver

a streaming interface for archive generation
https://www.archiverjs.com
MIT License
2.8k stars 220 forks source link

Handling error event prevents archive from completing #575

Open DanielStout5 opened 2 years ago

DanielStout5 commented 2 years ago

This seems to be related to https://github.com/archiverjs/node-archiver/issues/252 and possibly https://github.com/archiverjs/node-archiver/issues/321

I'm downloading files from S3 and appending them to the archive.

Some of these files might not exist, and I'd prefer not to have to check whether they exist before trying to add to the archive (both for performance, to avoid an unnecessary check, and for reliability, since the file could be deleted in between the check and the download).

It seems like this should be handled by just adding an event handler for the "error" event:

let zip = archiver("zip");
zip.on("error", (err) => {
  console.log("Zip error", err);
});

That does log the error, but it does not continue with the other files. Is there another way to get basically "Continue on error" behavior?

DanielStout5 commented 2 years ago

Using zip-stream directly instead of archiver fixed this for me.

I created my own function:

zipAppend(zip, data, opts) {
  return new Promise((resolve) => {
    zip.entry(data, opts, (err) => {
      if (err) {
        console.log("zipAppend error", err);
        if (!opts.ignoreError) throw err;
      }

      resolve();
    });
  });
},

Which, if opts.ignoreError is true, will just continue on if an error occurs, including if the S3 file isn't found.

artonio commented 1 year ago

This may not be perfect, but this is what we ended up doing:

import Archiver from "archiver/lib/core";
import {Logger} from "@nestjs/common";
import Zip from "archiver/lib/plugins/zip";

/***
 * This class extends the base Archiver class in order to catch a KeyNotFound error from S3.
 */
export class RArchiver extends Archiver {
    private readonly logger = new Logger(RArchiver.name, { timestamp: true });

    constructor() {
        super();
        const zip = Zip({ store: true });
        this.setModule(zip);
    }

    append(input, data) {
        super.append(input, data);
    }

    pipe(response) {
        super.pipe(response);
    }

    on(event: string, eventFunc) {
        super.on(event, eventFunc);
    }

    setModule(zip: Zip) {
        super.setModule(zip);
    }

    finalize() {
        super.finalize();
    }

    // override to not emit error
    _moduleAppend(source, data, callback) {
        // @ts-ignore
        if (this._state.aborted) {
            callback();
            return;
        }

        // @ts-ignore
        this._module.append(source, data, function (err) {
            this._task = null;

            if (this._state.aborted) {
                this._shutdown();
                return;
            }

            // !! Handle error here !!
            if (err) {
                // Original handling code
                // this.emit('error', err);

                // Log error
                this.logger.error(err);
                setImmediate(callback);
                return;
            }

            /**
             * Fires when the entry's input has been processed and appended to the archive.
             *
             * @event Archiver#entry
             * @type {EntryData}
             */
            this.emit('entry', data);
            this._entriesProcessedCount++;

            if (data.stats && data.stats.size) {
                this._fsEntriesProcessedBytes += data.stats.size;
            }

            /**
             * @event Archiver#progress
             * @type {ProgressData}
             */
            this.emit('progress', {
                entries: {
                    total: this._entriesCount,
                    processed: this._entriesProcessedCount
                },
                fs: {
                    totalBytes: this._fsEntriesTotalBytes,
                    processedBytes: this._fsEntriesProcessedBytes
                }
            });

            setImmediate(callback);
        }.bind(this));
    }
}