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

@modbox/s3-uploads-server

v0.2.1

Published

The backend package for s3-uploads module.

Downloads

14

Readme

@modbox/s3-uploads-server

Note: This is the server-side package for the s3-upload module, see @modbox/s3-upload-client for the client package.

s3-upload approach

This lib abstracts away the complexity of presigned requests uploads to an S3-compatible object storage service.
Based on two (backend and frontend) simple configurations that define onse or several upload types, for each of which your can :

  • Choose between simple or multipart upload
  • Whitelist a set of allowed mimetypes and sizes
  • Allow or forbid file replacement
  • Define a naming strategy and key conflict handler
  • Define a post-upload renaming strategy (e.g. your file should be ${postId}/${filename} and the postId is not generated until the upload is successful and the resource created in database)

Written in typescript with DX in mind.

Server-side usage

Setup and config


utils/upload.ts

import path from 'path'
import {
  setupUploadModule,
  FileToUpload,
  UploadMode,
  MimeTypesConfig,
  UploadedFile,
} from '@modbox/s3-uploads-server'

// Define your upload types, you can use an enum
export enum UploadType {
  Avatar = 'avatar',
  CommunityPost = 'community-post',
  ResourceFiles = 'resource-files',
}

const MULTIPART_CHUNK_SIZE = 10485760 // 10Mb = 10485760 bytes
const IMAGE_TYPE_CONDITION = {
  // Regex for all images
  types: [/^image\//],
  // Max size of 15MiB in bytes
  maxSize: 15728640,
}
const TEXT_TYPE_CONDITION = {
  // Strings for strict equality checks
  types: ['text/plain', 'another_mimetype', /some_regex/i],
  // Max size of 5MiB in bytes
  maxSize: 5242880,
}
const PDF_TYPE_CONDITION = {
  types: ['application/pdf'],
  // Max size of 50MiB in bytes
  maxSize: 52428800,
}

export const uploadModule = setupUploadModule({
  s3Config: {
    credentials: {
      accessKeyId: '<Your access key>',
      secretAccessKey: '<Your secret key>',
    },
    // Some S3-compatible object storage provider
    domain: '<Your cloud provider domain>',
    region: '<Your region>',
  },
  // Setting the multipart chunk size (defaults to 5MiB)
  multipartChunkSize: MULTIPART_CHUNK_SIZE,
  // Allowing this type for all uploads
  mimeTypesConfig: [IMAGE_TYPE_CONDITION],
  // Here define a condition where deletion is allowed to avoid data loss for example
  canDelete: env.NODE_ENV === 'production',
  uploads: {
    [UploadType.Avatar]: {
      bucket: env.SCW_AVATAR_BUCKET,
      mode: UploadMode.Single,
      buildKey: (
        file: FileToUpload,
        ctx: { userId: number; username: string },
      ) => {
        return `${ctx.userId}-${ctx.username}${path.extname(file.filename)}`
      },
    },
    [UploadType.CommunityPost]: {
      bucket: env.SCW_COMMUNITY_THREAD_BUCKET,
      mode: UploadMode.Multipart,
      // This allows you to build the key where the file will be stored based on current upload
      buildKey: (file: FileToUpload) => {
        return `tmp-${file.filename}`
      },
      /* When buildFinalKey is provided, you file once uploaded will be moved at this new key.
      This allows for example to create a resource in database only when upload is a success and then
      move the file around based on the new data (a generated id for example) */
      buildFinalKey: (
        file: UploadedFile,
        ctx: { postId: number; discussionId: number },
      ) => {
        return `${ctx.discussionId}/${ctx.postId}/${file.filename}`
      },
      /* This tells the lib how to rename the uploaded file when key is already taken
      If not provided and such case happens, the lib will throw (except if allowReplace is set to true) */
      onKeyConflict: (key: string, tryCount: number) => {
        const ext = path.extname(key)
        return `${key.substring(0, key.length - ext.length)}-${tryCount}${ext}`
      },
      // This adds PDF for this kind of uploads
      mimeTypesConfig: [PDF_TYPE_CONDITION],
    },
    [UploadType.ResourceFiles]: {
      // Upload the object even if it overrides an existing file
      allowReplace: true,
      bucket: env.SCW_RESOURCE_FILE_BUCKET,
      mode: UploadMode.Multipart,
      buildKey: (file: FileToUpload) => {
        return `tmp-${file.filename}`
      },
      buildFinalKey: (file: UploadedFile, ctx: { resourceId: number }) => {
        return `${ctx.resourceId}/${file.filename}`
      },
      // This adds TEXT files for this kind of uploads
      mimeTypesConfig: [TEXT_TYPE_CONDITION],
    },
  },
})

/* You can re-export some utils that are already setup with your provider credentials to use elsewhere in your app */
export const {
  buildBucketHostname,
  buildObjectUrl,
  deleteObject,
  doesObjectExist,
  downloadObject,
  getCleanFilename,
  moveObject,
} = uploadModule.utils

routes/users.ts

import express from 'express'
import { uploadModule, UploadTypes } from 'utils/upload.ts'

const app = express()
app.get('/avatar-upload-request', async (req, res) => {
  // Get user and file to upload from request
  const fileToUpload = req.body
  const user = req.user

  // Initiate upload
  const initiatedUpload = await uploadModule[UploadType.Avatar].initiateOne(
    fileToUpload,
    { userId: user.id, username: user.username },
  )
  res.json(initiatedUpload)
})

app.post('/users', async (req, res) => {
  // Get user and file to upload from request
  const uploadedFile = req.body
  const user = req.user

  // Complete upload
  const s3Object = await uploadModule[UploadType.Avatar].completeOne(
    uploadedFile,
  )
  const updatedUser = updateUserInDatabase({ ...user, avatar: s3Object })
  res.json(updatedUser)
})

app.listen(3000)

Configuration object


This is the definition of the config object passed to setupUploadModule.

| Property | Type | Required | Default | Description | | ------------------------- | --------------------------------------------------------------------------------------------------- | -------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | s3Config | { region: string; domain: string; credentials: { accessKeyId: string; secretAccessKey: string } } | true | | Pass you provider credentials and domain to setup the library (For some use cases, this accept the same config as aws-sdk, typechecking will help you) | | canDelete | boolean \| ((file: S3Location) => boolean \| Promise<boolean>) | false | true | This prevent object deletion to happen if passed false or a function that returns false | | multipartChunkSize | number | false | 5242880 (5MiB) | This number represent the chunk size for multipart uploads in bytes. | | mimeTypesConfig | Array<{ types: (string \| RegExp) \| (string \| RegExp)[]; maxSize: number }> | false | | Defines the list of allowed mime types and their corresponding max size. If not provided, all is accepted. | | uploads | See below for full breakdown of this object | true | | This object defines the different upload types as keys and their specific config as value. | | uploads.allowReplace | boolean | false | false | Allow or disallow object replacement (i.e. when you're uploading an object with the same key as an existing one). The lib will throw if false and uploads.onKeyConflict is not provided. | | uploads.bucket | string | true | | The bucket where these uploads should be stored | | uploads.buildKey | (fileToUpload: FileToUpload, ctx: any) => string | true | | Handler to build the object key based on file to upload | | uploads.buildFinalKey | (uploadedFile: UploadedFile, ctx: any) => string | false | | Handler that allow to rename the object once uploaded, useful in cases where you need some extra data to build the final key like an id not generated yet at the time of the initial upload. | | uploads.mimeTypesConfig | Same as root mimeTypesConfig | false | | Same as root mimeTypesConfig but this only applies to the upload type it's declared for (root mimeTypesConfig is applied to all specific upload types, no need to repeat it.) | | uploads.mode | UploadMode (enum exported by the lib) | true | | The upload to use for this upload type (can be UploadMode.Multipart \| UploadMode.Single) | | uploads.onKeyConflict | (key: string, tryCount: number, ctx: any) => string \| Promise<string> | false | | Handler for cases where an object already exist at the specified key. The lib will throw if not provided and uploads.allowReplace is false. |

Notes

This lib is still in development and will be considered production-ready when it reach version 1.0.0. Use at your own risk. Breaking changes might occur before 1.0.0, so you might want to use strict references in your package.json dependencies. After 1.0.0, semver will be rigorously respected.