npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@os-team/image-storage

v1.0.50

Published

Library for uploading images to a storage.

Downloads

167

Readme

@os-team/image-storage NPM version BundlePhobia

Library for uploading images to a storage.

Usually when you want to store an image you need:

  1. Validate the file type (e.g. allow only JPG, PNG, WEBP image formats). The file type should be detected by the first bytes, not by the file name (e.g. using the file-type library).
  2. Validate the file size.
  3. Convert the image to a specific extension (e.g. to JPG).
  4. Upload an image in multiple sizes to use them on the frontend side in different places (e.g. a large avatar on the profile page, a small avatar in the header).
  5. Crop some sizes of the image to use them, for example, in a list of blog posts.
  6. Append a hash to the file name to avoid caching when updating the image.
  7. Delete old images that have been replaced with new ones.

This library performs all these steps using an abstract storage, which must first be implemented (see below).

The following image storage implementations are currently available:

Usage

Step 1. Install the package

Install the package using the following command:

yarn add @os-team/image-storage

Step 2. Create a storage

The storage must implement the following interface:

export interface Storage {
  /**
   * Returns the list of files in a bucket starting with the specified prefix.
   */
  list(bucket: string, prefix: string): Promise<string[]>;

  /**
   * Uploads the file to a bucket with the specified name.
   */
  upload(options: StorageUploadOptions): Promise<void>;

  /**
   * Deletes a file.
   */
  delete(bucket: string, key: string): Promise<void>;

  /**
   * Deletes multiple files at once.
   */
  deleteMultiple(bucket: string, keys: string[]): Promise<void>;
}

For example, let's create the file storage in which images will be stored in the specified directory (bucket):

import { Storage, StorageUploadOptions } from '@os-team/image-storage';
import * as fs from 'fs';
import * as path from 'path';

class FileStorage implements Storage {
  public list(bucket: string, prefix: string): Promise<string[]> {
    return new Promise((resolve, reject) => {
      fs.readdir(bucket, (error, files) => {
        if (error) reject(error);
        else resolve(files.filter((file) => file.startsWith(prefix)));
      });
    });
  }

  public async upload(options: StorageUploadOptions): Promise<void> {
    const { bucket, key, body } = options;
    return new Promise((resolve, reject) => {
      const filePath = path.resolve(bucket, key);
      const writeStream = fs.createWriteStream(filePath);
      body.pipe(writeStream).on('error', reject).on('close', resolve);
    });
  }

  public delete(bucket: string, key: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const filePath = path.resolve(bucket, key);
      fs.unlink(filePath, (error) => {
        if (error) reject(error);
        else resolve(undefined);
      });
    });
  }

  public deleteMultiple(bucket: string, keys: string[]): Promise<void> {
    if (keys.length === 0) return Promise.resolve();
    let loadedFilesCount = 0;
    return new Promise((resolve, reject) => {
      keys.forEach((key) => {
        const filePath = path.resolve(bucket, key);
        fs.unlink(filePath, (error) => {
          if (error) reject(error);
          else if (loadedFilesCount < keys.length - 1) loadedFilesCount += 1;
          else resolve(undefined);
        });
      });
    });
  }
}

Step 3. Create an image storage

Your image storage must extend the abstract ImageStorage:

import ImageStorage, { ImageStorageOptions } from '@os-team/image-storage';

class FileImageStorage extends ImageStorage {
  protected readonly storage: Storage;

  public constructor(options: ImageStorageOptions) {
    super(options);
    this.storage = new FileStorage();
  }
}

Step 4. Use your image storage

import ImageStorage from '@os-team/image-storage';

const imageStorage: ImageStorage = new FileImageStorage();

// Upload an image
const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'name',
});

// Delete the image
await imageStorage.delete(name);

By default, the ImageStorage uploads the following images:

  • fileName-72
  • fileName-192
  • fileName-512
  • fileName-1024
  • fileName-2560
  • fileName-72-c
  • fileName-192-c
  • fileName-512-c
  • fileName-1024-c
  • fileName-2560-c

Customizations

Using a hash

To avoid caching, it is recommended to append a hash to the image name.

// name = 'name~hash'
const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'name',
  useHash: true,
});

The length of the hash is 4 characters.

Deleting old images

If you use a hash and upload an image with the same name for the second time, then the files will not be replaced. If an image stores in 10 different sizes (by default), then after the 10th upload of an image with the same name, there will be 100 files in a storage. To avoid this, it is necessary to delete the old files after the new image has been uploaded (not before, because the upload may fail).

You can delete old images as follows:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'name',
  useHash: true,
  deleteOldImages: true,
});

Uploading to a directory

By default, an image is uploaded in the root directory of a bucket. You can specify a directory inside the name, but in this case the upload method returns the name with the directory.

// name = 'dir1/dir2/name'
const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'dir1/dir2/name',
});

Usually, the image name is later stored in the database. In this case, the path is redundant. If you specify a directory in the path parameter, the upload method will return only the image name.

// name = 'name'
const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'name',
  path: 'dir1/dir2',
});

Changing image sizes

By default, an image is uploaded in the following sizes: 72, 192, 512, 1024, 2560. These sizes are used in most cases in any app (e.g. the Slack app uses similar image sizes).

You can specify your own sizes using sizes and croppedSizes options. The second one is used to determine in which sizes the cropped version of an image must be uploaded in a storage.

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  sizes: [50, 100],
  croppedSizes: [200],
});

If the croppedSizes option is not specified, it will be the same as sizes.

Setting the max size of an image

By default, the max size of an image is 20 MB. You can change it as follows:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  maxSize: 50 * 1024 * 1024, // In bytes
});

Setting allowed types of an image

By default, you can upload an image in any of the following extensions: jpg, png, webp, gif, avif, tif.

You can restrict allowed extensions as follows:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  types: ['jpg', 'png'], // Allow only JPG and PNG images to upload
});

Changing a suffix for cropped images

By default, the -c suffix is used for cropped images. You can change this suffix using the cropSuffix option:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  cropSuffix: '-cropped',
});

Now the image name will be fileName-72-cropped instead of fileName-72-c.

Setting concurrent files that uploads to a storage

By default, the max number of files that are uploaded in parallel is 4, but you can change it as follows:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  concurrentFiles: 10,
});

If you set sizes: [72, 192, 512, 1024, 2560], 10 files will be uploaded to a storage for each image (5 default + 5 cropped). The concurrency allows you to upload all image sizes faster, but the more simultaneous files are uploaded to a storage, the more memory is used.

Making base transformations

By default, an image is uploaded to a storage in progressive JPG with a quality of 90. If an image has a transparent background, it is set to white.

You can override these transformations as follows:

const { name } = await imageStorage.upload({
  body: createReadStream('my-image.jpg'),
  name: 'fileName',
  transformer: (sharp) =>
    sharp.flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({
      quality: 60, // Change the quality from 90 to 60
      progressive: true,
    }),
});

Deleting the specified image sizes

If you want to delete only the specified sizes of the image, pass them in the second parameter:

await imageStorage.delete(name, [50]);