@os-team/image-storage
v1.0.50
Published
Library for uploading images to a storage.
Downloads
167
Readme
@os-team/image-storage
Library for uploading images to a storage.
Usually when you want to store an image you need:
- 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).
- Validate the file size.
- Convert the image to a specific extension (e.g. to JPG).
- 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).
- Crop some sizes of the image to use them, for example, in a list of blog posts.
- Append a hash to the file name to avoid caching when updating the image.
- 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]);