meteor-slingshot - revived!
Direct and secure file-uploads to AWS S3, Google Cloud Storage and others.
Looking for maintainers
Please contact ferjep if you can help maintaining this package
Install
meteor add ferjep:slingshot
Meteor 3: This package is fully compatible with Meteor 3 (async/await, no Fibers). All directive callbacks (
authorize,key,pathPrefix,temporaryCredentials, etc.) may beasyncfunctions. See examples below.
Why?
There are many many packages out there that allow file uploads to S3, Google Cloud and other cloud storage services, but they usually rely on the meteor apps' server to relay the files to the cloud service, which puts the server under unnecessary load.
meteor-slingshot uploads the files directly to the cloud service from the browser without ever exposing your secret access key or any other sensitive data to the client and without requiring public write access to cloud storage to the entire public.
File uploads can not only be restricted by file-size and file-type, but also by other stateful criteria such as the current meteor user.
Quick Example
Client side
On the client side we can now upload files through to the bucket:
const uploader = new Slingshot.Upload("myFileUploads"); // send() returns a Promise that resolves with the download URL: try { const downloadUrl = await uploader.send(document.getElementById('input').files[0]); await Meteor.users.updateAsync(Meteor.userId(), { $push: { "profile.files": downloadUrl } }); } catch (error) { // Log service detailed response (only available for non-presigned uploads). console.error('Error uploading', uploader.xhr?.response); alert(error); }
A callback is also supported for backward compatibility:
uploader.send(document.getElementById('input').files[0], function (error, downloadUrl) { if (error) { console.error('Error uploading', uploader.xhr?.response); alert(error); } else { Meteor.users.updateAsync(Meteor.userId(), { $push: { "profile.files": downloadUrl } }); } });
Client and Server
These file upload restrictions are validated on the client and then appended to the directive on the server side to enforce them:
Slingshot.fileRestrictions("myFileUploads", { allowedFileTypes: ["image/png", "image/jpeg", "image/gif"], maxSize: 10 * 1024 * 1024 // 10 MB (use null for unlimited). });
Important: The fileRestrictions must be declared before the directive is instantiated.
Server side
On the server we declare a directive that controls upload access rules:
Slingshot.createDirective("myFileUploads", Slingshot.S3Storage, { bucket: "mybucket", // This may be a String or a function acl: "public-read", // 'STANDARD' or 'REDUCED_REDUNDANCY' storageClass: 'REDUCED_REDUNDANCY', authorize: function () { //Deny uploads if user is not logged in. if (!this.userId) { const message = "Please login before posting files"; throw new Meteor.Error("Login Required", message); } return true; }, key: async function (file) { //Store file into a directory by the user's username. const user = await Meteor.users.findOneAsync(this.userId); return user.username + "/" + file.name; } });
With the directive above, no other files than images will be allowed. The policy is directed by the meteor app server and enforced by AWS S3.
Note: If your bucket is created in any region other than US Standard, you will need to set the region key in the directive. Refer the AWS Slingshot Storage Directives
Storage services
The client side is agnostic to which storage service is used. All it needs for the file upload to work, is a directive name.
There is no limit imposed on how many directives can be declared for each storage service.
Storage services are pluggable in Slingshot and you can add support for own storage service as described in a section below.
Progress bars
You can create file upload progress bars as follows:
<template name="progressBar"> <div class="progress"> <div class="progress-bar" role="progressbar" aria-valuenow="" aria-valuemin="0" aria-valuemax="100" style="width: %;"> <span class="sr-only">% Complete</span> </div> </div> </template>
Using the Slingshot.Upload instance read and react to the progress:
Template.progressBar.helpers({ progress: function () { return Math.round(this.uploader.progress() * 100); } });
Show uploaded file before it is uploaded (latency compensation)
<template name="myPicture"> <img src=/> </template>
Template.myPicture.helpers({ url: function () { //If we are uploading an image, pass true to download the image into cache. //This will preload the image before using the remote image url. return this.uploader.url(true); } });
This to show the image from the local source until it is uploaded to the server.
If Blob URL's are not available it will attempt to use FileReader to generate
a base64-encoded url representing the data as a fallback.
Add meta-context to your uploads
You can add meta-context to your file-uploads, to make your requests more specific on where the files are to be uploaded.
Consider the following example...
We have an app that features picture albums. An album belongs to a user and only that user is allowed to upload picture to it. In the cloud each album has its own directory where its pictures are stored.
We declare our client-side uploader as follows:
const metaContext = {albumId: album._id} const uploadToMyAlbum = new Slingshot.Upload("picturealbum", metaContext);
On the server side the directive can now set the key accordingly and check if the user is allowed post pictures to the given album:
Slingshot.createDirective("picturealbum", Slingshot.GoogleCloud, { acl: "public-read", authorize: async function (file, metaContext) { const album = await Albums.findOneAsync(metaContext.albumId); //Denied if album doesn't exist or if it is not owned by the current user. return album && album.userId === this.userId; }, key: function (file, metaContext) { return metaContext.albumId + "/" + Date.now() + "-" + file.name; } });
Manual Client Side validation
You can check if a file uploadable according to file-restrictions as follows:
const uploader = new Slingshot.Upload("myFileUploads"); const error = await uploader.validate(document.getElementById('input').files[0]); if (error) { console.error(error); }
The validate method will return null if valid and returns an Error instance
if validation fails.
AWS S3
You will need aAWSAccessKeyId and AWSSecretAccessKey in Meteor.settings
and a bucket with the following CORS configuration:
1[ 2 { 3 "AllowedHeaders": [ 4 "Authorization" 5 ], 6 "AllowedMethods": [ 7 "PUT", 8 "GET", 9 "POST", 10 "HEAD", 11 "DELETE" 12 ], 13 "AllowedOrigins": [ 14 "your-server-goes-here" 15 ], 16 "ExposeHeaders": [], 17 "MaxAgeSeconds": 3000 18 } 19]
Declare AWS S3 Directives as follows:
Slingshot.createDirective("aws-s3-example", Slingshot.S3Storage, { //... });
S3 with temporary AWS Credentials (Advanced)
For extra security you can use temporary credentials to sign upload requests.
import { STS } from "@aws-sdk/client-sts"; const stsClient = new STS({ region: "us-east-1" }); Slingshot.createDirective('myUploads', Slingshot.S3Storage.TempCredentials, { bucket: 'myBucket', async temporaryCredentials(expire) { // AWS dictates that the minimum duration must be 900 seconds: const duration = Math.max(Math.round(expire / 1000), 900); const result = await stsClient.getSessionToken({ DurationSeconds: duration }); return result.Credentials; // { AccessKeyId, SecretAccessKey, SessionToken } } });
If you are running slingshot on an EC2 instance, you can conveniently retrieve your access keys from the instance metadata:
import { STS } from "@aws-sdk/client-sts"; Slingshot.createDirective('myUploads', Slingshot.S3Storage.TempCredentials, { bucket: 'myBucket', async temporaryCredentials(expire) { const duration = Math.max(Math.round(expire / 1000), 900); // The STS client on EC2 automatically uses the instance role credentials. const stsClient = new STS({ region: "us-east-1" }); const result = await stsClient.getSessionToken({ DurationSeconds: duration }); return result.Credentials; } });
S3 Presigned PUT URLs (Slingshot.S3Storage.PresignedUrl and Slingshot.S3Storage.TempCredentials.PresignedUrl)
The most secure option: the server generates a short-lived presigned PUT URL scoped to a single object key. The browser uploads directly to that URL — no AWS credentials (access key, secret, or session token) are ever sent to the client.
A leaked presigned URL can only upload to one specific path and expires after
the directive's expire duration.
Using static keys (same as S3Storage — uses AWSAccessKeyId /
AWSSecretAccessKey directly):
Slingshot.createDirective('myUploads', Slingshot.S3Storage.PresignedUrl, { bucket: 'myBucket', region: 'us-east-1', AWSAccessKeyId: Meteor.settings.AWSAccessKeyId, AWSSecretAccessKey: Meteor.settings.AWSSecretAccessKey, acl: 'private', key(file, meta) { return `uploads/${this.userId}/${Date.now()}-${file.name}`; } });
Using temporary credentials — a drop-in upgrade from TempCredentials.
The temporaryCredentials function is reused exactly as-is:
import { STS } from "@aws-sdk/client-sts"; const stsClient = new STS({ region: "us-east-1" }); Slingshot.createDirective('myUploads', Slingshot.S3Storage.TempCredentials.PresignedUrl, { bucket: 'myBucket', region: 'us-east-1', async temporaryCredentials(expire) { const duration = Math.max(Math.round(expire / 1000), 900); const result = await stsClient.getSessionToken({ DurationSeconds: duration }); return result.Credentials; }, key(file, meta) { return `uploads/${this.userId}/${Date.now()}-${file.name}`; } });
CORS: add
"PUT"to your bucket'sAllowedMethodswhen using presigned URL directives. ThePOSTentry can be kept for other directives or removed if you've migrated everything.
S3 Server-Side Encryption (SSE)
You can enable server-side encryption by setting the "sse" key in the metaContext accordingly.
const uploadToMyAlbum = new Slingshot.Upload("picturealbum", { sse: true });
The "sse" key can take one of the following values:
true //enables AWS-managed AES-256 encryption keys {kms: true, kmsKeyId: YOUR_KMS_KEY_ID} //enables AWS-managed KMS encryption keys. If the kmsKeyId is not specified, the master key will be used {key: YOUR_AES256_KEY} //enables SSE with customer-provided keys
Google Cloud
Generate a private key and convert it to a .pem file
using openssl:
openssl pkcs12 -in google-cloud-service-key.p12 -nodes -nocerts > google-cloud-service-key.pem
Setup CORS on the bucket:
gsutil cors set docs/gs-cors.json gs://mybucket
Save this file into the /private directory of your meteor app and add this
line to your server-side code:
Meteor.startup(async () => { Slingshot.GoogleCloud.directiveDefault.GoogleSecretKey = await Assets.getTextAsync('google-cloud-service-key.pem'); });
Declare Google Cloud Storage Directives as follows:
Slingshot.createDirective("google-cloud-example", Slingshot.GoogleCloud, { //... });
Rackspace Cloud Files
You will need aRackspaceAccountId (your acocunt number) and
RackspaceMetaDataKey in Meteor.settings.
In order to obtain your RackspaceMetaDataKey (a.k.a. Account-Meta-Temp-Url-Key)
you need an
auth-token
and then follow the
instructions here.
Note that API-Key, Auth-Token, Meta-Data-Key are not the same thing:
API-Key is what you need to obtain an Auth-Token, which in turn is what you need to setup CORS and to set your Meta-Data-Key. The auth-token expires after 24 hours.
For your directive you need container and provide its name, region and cdn.
Slingshot.createDirective("rackspace-files-example", Slingshot.RackspaceFiles, { container: "myContainer", //Container name. region: "lon3", //Region code (The default would be 'iad3'). //You must set the cdn if you want the files to be publicly accessible: cdn: "https://abcdefghije8c9d17810-ef6d926c15e2b87b22e15225c32e2e17.r19.cf5.rackcdn.com", pathPrefix: async function (file) { //Store file into a directory by the user's username. const user = await Meteor.users.findOneAsync(this.userId); return user.username; } });
To setup CORS you also need to your Auth-Token from above and use:
curl -I -X POST -H 'X-Auth-Token: yourAuthToken' \ -H 'X-Container-Meta-Access-Control-Allow-Origin: *' \ -H 'X-Container-Meta-Access-Expose-Headers: etag location x-timestamp x-trans-id Access-Control-Allow-Origin' \ https://storage101.containerRegion.clouddrive.com/v1/MossoCloudFS_yourAccoountNumber/yourContainer
Cloudinary
Cloudinary is supported via a 3rd party package. jimmiebtlr:cloudinary
Browser Compatibility
Currently the uploader uses XMLHttpRequest 2 to upload the files, which is not
supported on Internet Explorer 9 and older versions of Internet Explorer.
This can be circumvented by falling back to iframe uploads in future versions, if required.
Latency compensation is available in Internet Explorer 10.
Security
The secret key never leaves the meteor app server. Nobody will be able to upload anything to your buckets outside of your meteor app.
Standard services (S3Storage, TempCredentials): Slingshot uses a policy
document signed by the secret key and sends it to the browser along with the
upload credentials. The policy contains all the restrictions defined in the
directive and expires after expire milliseconds (default 5 minutes).
Presigned URL services (S3Storage.PresignedUrl,
S3Storage.TempCredentials.PresignedUrl): The server generates a presigned PUT
URL using the AWS SDK and sends only that URL to the browser. No credentials
— not even temporary ones — are ever transmitted to the client. The URL is
cryptographically scoped to a single object key and expires after expire
milliseconds. This is the recommended approach for new applications.
Adding Support for other storage Services
Cloud storage services are pluggable in Slingshot. You can add support for a cloud storage service of your choice. All you need is to declare an object with the following parameters:
MyStorageService = { /** * Define the additional parameters that your your service uses here. * * Note that some parameters like maxSize are shared by all services. You do * not need to define those by yourself. */ directiveMatch: { accessKey: String, options: Object, foo: Match.Optional(Function) }, /** * Here you can set default parameters that your service will use. */ directiveDefault: { options: {} }, /** * * @param {Object} method - This is the Meteor Method context. * @param {Object} directive - All the parameters from the directive. * @param {Object} file - Information about the file as gathered by the * browser. * @param {Object} [meta] - Meta data that was passed to the uploader. * * @returns {UploadInstructions} */ upload: async function (method, directive, file, meta) { const accessKey = directive.accessKey; const fooData = directive.foo && await directive.foo.call(method, file, meta); //Here you need to make sure that all parameters passed in the directive //are going to be enforced by the server receiving the file. return { // Endpoint where the file is to be uploaded: upload: "https://example.com", // Download URL, once the file uploaded: download: directive.cdn || "https://example.com/" + file.name, // POST data to be attached to the file-upload: postData: [ { name: "accessKey", value: accessKey }, { name: "signature", value: signature } //... ], // HTTP headers to send when uploading: headers: { "x-foo-bar": fooData } }; }, /** * Absolute maximum file-size allowable by the storage service. */ maxSize: 5 * 1024 * 1024 * 1024 };
Example Directive:
Slingshot.createDirective("myUploads", MyStorageService, { accessKey: "a12345xyz", foo: function (file, metaContext) { return "bar"; } });
Dependencies
Meteor core packages:
- tracker
- reactive-var
- check
Troubleshooting and Help
If you are having any queries about how to use slingshot, or how to get it to work with the different services or any other general questions about it, please post a question on Stack Overflow. You will get a high quality answer there much quicker than by posting an issue here on github.
Bug reports, Feature Requests and Pull Requests are always welcome.
API Reference
Directives
General (All Services)
authorize: Function (required unless set in File Restrictions)
maxSize: Number (required unless set in File Restrictions)
allowedFileTypes RegExp, String or Array (required unless set in File
Restrictions)
cdn String (optional) - CDN domain for downloads.
i.e. "https://d111111abcdef8.cloudfront.net"
expire Number (optional) - Number of milliseconds in which an upload
authorization will expire after the request was made. Default is 5 minutes.
AWS S3 (Slingshot.S3Storage)
bucket String or Function (required) - Name of bucket to use. For AWS S3
the default bucket is Meteor.settings.S3Bucket.
If a function is provided, it will be called with userId in the context and
its return value is used as the bucket. First argument is file info and the
second is the meta-information that can be passed by the client.
region String or Function(optional) - Default is Meteor.settings.AWSRegion or
"us-east-1". See AWS Regions
If a function is provided, it will be called with userId in the context and
its return value is used as the region. First argument is file info and the
second is the meta-information that can be passed by the client.
AWSAccessKeyId String (required) - Can also be set in Meteor.settings.
AWSSecretAccessKey String (required) - Can also be set in Meteor.settings.
AWS S3 with Temporary Credentials (Slingshot.S3Storage.TempCredentials)
bucket String or Function (required) - Name of bucket to use. For AWS S3
the default bucket is Meteor.settings.S3Bucket.
If a function is provided, it will be called with userId in the context and
its return value is used as the bucket. First argument is file info and the
second is the meta-information that can be passed by the client.
region String or Function(optional) - Default is Meteor.settings.AWSRegion or
"us-east-1". See AWS Regions
If a function is provided, it will be called with userId in the context and
its return value is used as the region. First argument is file info and the
second is the meta-information that can be passed by the client.
temporaryCredentials Async Function (required) - Function that generates temporary
credentials. It takes a signle argument, which is the minumum desired expiration
time in milli-seconds and it returns an object that contains AccessKeyId,
SecretAccessKey and SessionToken.
AWS S3 Presigned PUT URL (Slingshot.S3Storage.PresignedUrl)
Server-side presigned URL variant using static long-lived keys. The browser receives only a signed URL — no credentials are transmitted to the client.
bucket String or Function (required) - Same as S3Storage.
region String or Function (optional) - Same as S3Storage.
AWSAccessKeyId String (required) - Can also be set in Meteor.settings.
AWSSecretAccessKey String (required) - Can also be set in Meteor.settings.
CORS: add
"PUT"to your S3 bucket'sAllowedMethods.
AWS S3 Presigned PUT URL with Temporary Credentials (Slingshot.S3Storage.TempCredentials.PresignedUrl)
Server-side presigned URL variant using temporary credentials. Drop-in upgrade
from S3Storage.TempCredentials — the temporaryCredentials function is
reused as-is. No credentials are transmitted to the client.
bucket String or Function (required) - Same as S3Storage.TempCredentials.
region String or Function (optional) - Same as S3Storage.TempCredentials.
temporaryCredentials Async Function (required) - Same as
S3Storage.TempCredentials. Returns { AccessKeyId, SecretAccessKey, SessionToken }.
CORS: add
"PUT"to your S3 bucket'sAllowedMethods.
Google Cloud Storage (Slingshot.GoogleCloud)
bucket String (required) - Name of bucket to use. The default is
Meteor.settings.GoogleCloudBucket.
GoogleAccessId String (required) - Can also be set in Meteor.settings.
GoogleSecretKey String (required) - Can also be set in Meteor.settings.
AWS S3 and Google Cloud Storage
bucketUrl String or Function (optional) - Override URL to which files are
uploaded. If it is a function, then the first argument is the bucket name. This
url also used for downloads unless a cdn is given.
key String or Function (required) - Name of the file on the cloud storage
service. If a function is provided, it will be called with userId in the
context and its return value is used as the key. First argument is file info and
the second is the meta-information that can be passed by the client.
acl String (optional)
cacheControl String (optional) - RFC 2616 Cache-Control directive
contentDisposition String or Function (optional) - RFC 2616
Content-Disposition directive. Default is the uploaded file's name (inline). If
it is a function then it takes the same context and arguments as the key
function. Use null to disable.
Rackspace Cloud (Slingshot.RackspaceFiles)
RackspaceAccountId String (required) - Can also be set in Meteor.settings.
RackspaceMetaDataKey String (required) - Can also be set in Meteor.settings.
container String (required) - Name of container to use.
region String (optional) - Data Center region. The default is "iad3".
See other regions
pathPrefix String or Function (required) - Similar to key for S3, but
will always be appended by file.name that is provided by the client.
deleteAt Date (optional) - Absolute time when the uploaded file is to be
deleted. This attribute is not enforced at all. It can be easily altered by the
client
deleteAfter Number (optional) - Same as deleteAt, but relative.
File restrictions
authorize Function (optional) - Function to determines if upload is allowed.
maxSize Number (optional) - Maximum file-size (in bytes). Use null or 0
for unlimited.
allowedFileTypes RegExp, String or Array (optional) - Allowed MIME types. Use
null for any file type.