Fork me on GitHub

Files and UploadFS

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this step we are going to add the ability to upload images into our app, and also sorting and naming them.

Angular-Meteor can use Meteor UploadFS which is a suite of Meteor packages that together provide a complete file management solution including uploading, downloading, storage, synchronization, manipulation, and copying.

It supports several storage adapters for saving files to the local filesystem, GridFS and additional storage adapters can be created.

The process is very similar for handling any other MongoDB Collection!

So let's add image upload to our app!

We will start by adding UploadFS to our project, by running the following command:

$ meteor add jalik:ufs

Now, we will decide the storage adapter we want to use. In this example, we will use the GridFS as storage adapters, so we will add the adapter by running this command:

$ meteor add jalik:ufs-gridfs

Note: you can find more information about Stores and Storage Adapters on the UploadFS's GitHub repository.

So now we have the UploadFS support and the storage adapter installed - we still need to create a UploadFS object to handle our files. Note that you will need to define the collection as shared resource because you will need to use the collection in both client and server side.

Creating the Mongo Collection and UploadFS Store

Let's start by creating imports/api/images/collection.js file, and define a Mongo Collection object called "Images". Since we want to be able to make thumbnails we have to create another Collection called "Thumbs".

Also we will use the stadard Mongo Collection API that allows us to defined auth-rules.

20.3 Create Images and Thumbs collections imports/api/images/collection.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
 
export const Images = new Mongo.Collection('images');
export const Thumbs = new Mongo.Collection('thumbs');
 
function loggedIn(userId) {
  return !!userId;
}
 
Thumbs.allow({
  insert: loggedIn,
  update: loggedIn,
  remove: loggedIn
});
 
Images.allow({
  insert: loggedIn,
  update: loggedIn,
  remove: loggedIn
});

We have to create Stores for Images and Thumbs.

20.4 Create ThumbsStore and ImagesStore imports/api/images/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { UploadFS } from 'meteor/jalik:ufs';
import { Images, Thumbs } from './collection';
 
export const ThumbsStore = new UploadFS.store.GridFS({
  collection: Thumbs,
  name: 'thumbs'
});
 
export const ImagesStore = new UploadFS.store.GridFS({
  collection: Images,
  name: 'images',
  filter: new UploadFS.Filter({
    contentTypes: ['image/*']
  }),
  copyTo: [
    ThumbsStore
  ]
});

Let's explain a bit what happened.

  • We assigned Stores to their Collections, which is required.
  • We defined names of these Stores.
  • We added filter to ImagesStore so it can receive only images.
  • Every file will be copied to ThumbsStore.

Now we can create index.js file to export Collections and Stores:

20.5 Create main export file of Images and Thumbs imports/api/images/index.js
1
2
export * from './collection';
export * from './store';

There is a reason why we called one of the Collections the Thumbs!

Since we transfer every uploaded file to ThumbsStore, we can now easily add file manipulations.

Let's resize every file to 32x32:

20.6 Resize images imports/api/images/store.js
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
export const ThumbsStore = new UploadFS.store.GridFS({
  collection: Thumbs,
  name: 'thumbs',
  transformWrite(from, to, fileId, file) {
    // Resize to 32x32
    const gm = require('gm');
 
    gm(from, file.name)
      .resize(32, 32)
      .gravity('Center')
      .extent(32, 32)
      .quality(75)
      .stream()
      .pipe(to);
  }
});
 
export const ImagesStore = new UploadFS.store.GridFS({

We used gm module, let's install it:

$ meteor npm install gm --save

Be aware that this module requires at least one of these libraries: GraphicsMagick, ImageMagick.

Image upload

Now, let's create the PartyUpload component. It will be responsible for cropping and uploading photos.

20.8 Create view for PartyUpload imports/ui/components/partyUpload/partyUpload.html
1
2
3
4
5
6
7
8
9
<div layout="column">
  <div>
      <div>Click here to select image</div>
      <div>
        <strong>OR</strong>
      </div>
      <div>You can also drop image to here</div>
  </div>
</div>
20.9 Create PartyUpload component imports/ui/components/partyUpload/partyUpload.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import { Meteor } from 'meteor/meteor';
 
import template from './partyUpload.html';
 
class PartyUpload {}
 
const name = 'partyUpload';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  controllerAs: name,
  controller: PartyUpload
});

We want to use it in PartyAdd:

5
6
7
8
9
10
11
 
32
33
34
35
36
37
38
39
 
import template from './partyAdd.html';
import { Parties } from '../../../api/parties';
import { name as PartyUpload } from '../partyUpload/partyUpload';
 
class PartyAdd {
  constructor() {
...some lines skipped...
 
// create a module
export default angular.module(name, [
  angularMeteor,
  PartyUpload
]).component(name, {
  template,
  bindings: {
20.11 Implement to the view imports/ui/components/partyAdd/partyAdd.html
19
20
21
22
23
  <div flex>
    <md-button ng-click="partyAdd.submit()" class="md-raised">Add Party!</md-button>
  </div>
  <party-upload files="partyAdd.party.images"></party-upload>
</div>

As you can see we used files directive. We will create it later. For now, let's only say that this is a two-way data binding.

Note that for file upload you can use basic HTML <input type="file"> or any other package - you only need the HTML5 File object to be provided.

For our application, we would like to add ability to drag-and-drop images, so we use AngularJS directive that handles file upload and gives us more abilities such as drag & drop, file validation on the client side. In this example, We will use ng-file-upload, which have many features for file upload. In order to do this, let's add the package to our project:

$ meteor npm install ng-file-upload --save

Now, lets add a dependency in the PartyUpload:

1
2
3
4
5
6
 
12
13
14
15
16
17
18
19
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
 
// create a module
export default angular.module(name, [
  angularMeteor,
  ngFileUpload
]).component(name, {
  template,
  controllerAs: name,

Now, let's add the usage of ng-file-upload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div layout="column">
  <div ngf-drop
    ngf-select
    ngf-change="partyUpload.addImages($files)"
    ngf-drag-over-class="{accept:'dragover', reject:'dragover-err', delay:100}"
    class="drop-box"
    ngf-multiple="false"
    ngf-allow-dir="false"
    ngf-accept="'image/*'"
    ngf-drop-available="true"
    ng-hide="partyUpload.cropImgSrc">
    <div>Click here to select image</div>
    <div>
      <strong>OR</strong>
    </div>
    <div>You can also drop image to here</div>
  </div>
</div>

Now let's make it better looking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
party-upload {
  .drop-box {
    background: #F8F8F8;
    border: 5px dashed #DDD;
    text-align: center;
    padding: 25px;
    margin-left: 10px;
    margin-bottom: 20px;
  }
  .drop-box.dragover {
    border: 5px dashed blue;
  }
  .drop-box.dragover-err {
    border: 5px dashed red;
  }
}

then import new .less file in the PartyAdd

1
@import "../partyUpload/partyUpload.less";

and in the PartyAddButton:

3
4
5
6
  right: 15px;
  bottom: 15px;
}
@import "../partyAdd/partyAdd.less";

And that's it! now we can upload images by using drag and drop! Just note that the Application UI still don't show the new images we upload... we will add this later. Now let's add some more cool features, And make the uploaded image visible!

Image Crop

One of the most common actions we want to make with pictures is to edit them before saving. We will add to our example ability to crop images before uploading them to the server, using ngImgCrop package. So lets start by adding the package to our project:

$ meteor npm install ng-img-crop --save

And add a dependency in our module:

1
2
3
4
5
6
7
8
9
 
39
40
41
42
43
44
45
46
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
import 'ng-img-crop/compile/minified/ng-img-crop';
import 'ng-img-crop/compile/minified/ng-img-crop.css';
 
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  ngFileUpload,
  'ngImgCrop'
]).component(name, {
  template,
  controllerAs: name,

We want to perform the crop on the client, before uploading it, so let's get the uploaded image, and instead of saving it to the server - we will get the Data Url of it, and use it in the ngImgCrop:

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
import template from './partyUpload.html';
 
class PartyUpload {
  constructor($scope, $reactive) {
    'ngInject';
 
    $reactive(this).attach($scope);
  }
 
  addImages(files) {
    if (files.length) {
      this.currentFile = files[0];
 
      const reader = new FileReader;
 
      reader.onload = this.$bindToContext((e) => {
        this.cropImgSrc = e.target.result;
        this.myCroppedImage = '';
      });
 
      reader.readAsDataURL(files[0]);
    } else {
      this.cropImgSrc = undefined;
    }
  }
}
 
const name = 'partyUpload';
 

We took the file object and used HTML5 FileReader API to read the file from the user on the client side, without uploading it to the server. Then we saved the DataURL of the image into a variable cropImgSrc. Next, we will need to use this DataURI with the ngImgCrop directive as follow:

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    </div>
    <div>You can also drop image to here</div>
  </div>
  <div ng-show="partyUpload.cropImgSrc" layout="column" class="ng-crop">
    <div>
      <h3>Edit &amp; crop</h3>
      <md-button ng-click="partyUpload.save()" class="md-primary">
        Save Image
      </md-button>
      <md-button ng-click="partyUpload.reset()">
        Cancel
      </md-button>
    </div>
    <div class="ng-crop-container">
      <img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
    </div>
  </div>
</div>

Moreover we add ng-hide to the upload control, in order to hide it, after the user picks an image to crop.

And add some CSS to make it look better:

13
14
15
16
17
18
19
20
21
22
23
24
25
  .drop-box.dragover-err {
    border: 5px dashed red;
  }
  .ng-crop {
    h3 {
      margin-top: 0;
    }
  }
  .ng-crop-container {
    width: 400px;
    height: 225px;
  }
}

Now we need to handle uploading files.

Since we're using DataURL and we need UploadFS expects ArrayBuffer let's add few helper methods.

Add dataURLToBlob to converts DataURL to Blob object:

20.23 Add dataURLToBlob helper imports/api/images/helpers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * Converts DataURL to Blob object
 *
 * https://github.com/ebidel/filer.js/blob/master/src/filer.js#L137
 *
 * @param  {String} dataURL
 * @return {Blob}
 */
export function dataURLToBlob(dataURL) {
    const BASE64_MARKER = ';base64,';
 
    if (dataURL.indexOf(BASE64_MARKER) === -1) {
      const parts = dataURL.split(',');
      const contentType = parts[0].split(':')[1];
      const raw = decodeURIComponent(parts[1]);
 
      return new Blob([raw], {type: contentType});
    }
 
    const parts = dataURL.split(BASE64_MARKER);
    const contentType = parts[0].split(':')[1];
    const raw = window.atob(parts[1]);
    const rawLength = raw.length;
    const uInt8Array = new Uint8Array(rawLength);
 
    for (let i = 0; i < rawLength; ++i) {
      uInt8Array[i] = raw.charCodeAt(i);
    }
 
    return new Blob([uInt8Array], {type: contentType});
}

We have now Blob object so let's convert it to expected ArrayBuffer by creating a function called blobToArrayBuffer:

20.24 Add blobToArrayBuffer helper imports/api/images/helpers.js
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 
    return new Blob([uInt8Array], {type: contentType});
}
 
/**
 * Converts Blob object to ArrayBuffer
 *
 * @param  {Blob}       blob          Source file
 * @param  {Function}   callback      Success callback with converted object as a first argument
 * @param  {Function}   errorCallback Error callback with error as a first argument
 */
export function blobToArrayBuffer(blob, callback, errorCallback) {
  const reader = new FileReader();
 
  reader.onload = (e) => {
    callback(e.target.result);
  };
 
  reader.onerror = (e) => {
    if (errorCallback) {
      errorCallback(e);
    }
  };
 
  reader.readAsArrayBuffer(blob);
}

Now we can take care of uploading by creating a upload function:

20.25 Create `upload` method imports/api/images/methods.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { UploadFS } from 'meteor/jalik:ufs';
import { ImagesStore } from './store';
import { dataURLToBlob, blobToArrayBuffer } from './helpers';
 
/**
 * Uploads a new file
 *
 * @param  {String}   dataUrl [description]
 * @param  {String}   name    [description]
 * @param  {Function} resolve [description]
 * @param  {Function} reject  [description]
 */
export function upload(dataUrl, name, resolve, reject) {
  // convert to Blob
  const blob = dataURLToBlob(dataUrl);
  blob.name = name;
 
  // pick from an object only: name, type and size
  const file = _.pick(blob, 'name', 'type', 'size');
 
  // convert to ArrayBuffer
  blobToArrayBuffer(blob, (data) => {
    const upload = new UploadFS.Uploader({
      data,
      file,
      store: ImagesStore,
      onError: reject,
      onComplete: resolve
    });
 
    upload.start();
  }, reject);
}
  • dataUrl is the file we want to upload
  • name is a name of the file
  • resolve is a callback function that will be invoked on success
  • reject is a callback function that will be invoked on error
  • We took name, type and size from Blob object to send these information to UploadFS.Uploader

What's left is just to export this method:

20.26 Export methods imports/api/images/index.js
1
2
3
export * from './collection';
export * from './store';
export * from './methods';

We previously defined two methods: save() and reset(). Let's implement them using recently created upload function:

20.27 Implement `save` and `reset` methods imports/ui/components/partyUpload/partyUpload.js
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { Meteor } from 'meteor/meteor';
 
import template from './partyUpload.html';
import { upload } from '../../../api/images';
 
class PartyUpload {
  constructor($scope, $reactive) {
    'ngInject';
 
    $reactive(this).attach($scope);
 
    this.uploaded = [];
  }
 
  addImages(files) {
...some lines skipped...
      this.cropImgSrc = undefined;
    }
  }
 
  save() {
    upload(this.myCroppedImage, this.currentFile.name, this.$bindToContext((file) => {
      this.uploaded.push(file);
      this.reset();
    }), (e) => {
      console.log('Oops, something went wrong', e);
    });
  }
 
  reset() {
    this.cropImgSrc = undefined;
    this.myCroppedImage = '';
  }
}
 
const name = 'partyUpload';

Let's explain the code.

  • The cropped image is under myCroppedImage variable so we used it to as the file to be uploaded.
  • We used currentFile.name to get name of the file.
  • We used $bindToContext on the first callback (resolve) to keep it inside digest cycle.
  • We used console.log to notify about an error.
  • We created `reset()`` to clear these variables.

Last thing to do is to import Images and Thumb on the server-side:

20.28 Import Images on server side server/main.js
1
2
3
4
import '../imports/startup/fixtures';
import '../imports/api/parties';
import '../imports/api/users';
import '../imports/api/images';

Display Uploaded Images

Let's create a simple gallery to list the images in the new party form:

29
30
31
32
33
34
35
36
37
      <img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
    </div>
  </div>
  <div layout="row" class="images-container-title">
    <div class="party-image-container" ng-class="{'main-image': $index === 0}" ng-repeat="thumb in partyUpload.thumbs">
      <img ng-src="{{ thumb.url }}"/>
    </div>
  </div>
</div>
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    width: 400px;
    height: 225px;
  }
  .images-container-title {
    padding-bottom: 30px;
  }
  .party-image-container {
    position: relative;
    margin-right: 10px;
    max-height: 200px;
    max-width: 200px;
    img {
      max-height: 100%;
      max-width: 100%;
    }
  }
  .main-image {
    border: 2px solid #ddd;
  }
}

We have to somehow get thumbnails to show them in this gallery.

Since we keep them in Collections, we have to create proper publications:

20.31 Add `thumbs` and `images` publications imports/api/images/publish.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Meteor } from 'meteor/meteor';
import { Thumbs, Images } from './collection';
 
if (Meteor.isServer) {
  Meteor.publish('thumbs', function(ids) {
    return Thumbs.find({
      originalStore: 'images',
      originalId: {
        $in: ids
      }
    });
  });
 
  Meteor.publish('images', function() {
    return Images.find({});
  });
}
20.32 Import publications imports/api/images/index.js
1
2
3
4
import './publish';
export * from './collection';
export * from './store';
export * from './methods';

Now we can implement thumbnails:

8
9
10
11
12
13
14
 
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
54
55
56
57
58
59
60
61
62
63
64
65
import { Meteor } from 'meteor/meteor';
 
import template from './partyUpload.html';
import { Thumbs, upload } from '../../../api/images';
 
class PartyUpload {
  constructor($scope, $reactive) {
...some lines skipped...
    $reactive(this).attach($scope);
 
    this.uploaded = [];
 
    this.subscribe('thumbs', () => [
      this.getReactively('files', true) || []
    ]);
 
    this.helpers({
      thumbs() {
        return Thumbs.find({
          originalStore: 'images',
          originalId: {
            $in: this.getReactively('files', true) || []
          }
        });
      }
    });
  }
 
  addImages(files) {
...some lines skipped...
  save() {
    upload(this.myCroppedImage, this.currentFile.name, this.$bindToContext((file) => {
      this.uploaded.push(file);
 
      if (!this.files || !this.files.length) {
        this.files = [];
      }
      this.files.push(file._id);
 
      this.reset();
    }), (e) => {
      console.log('Oops, something went wrong', e);

There are few things that need to be explained:

  • We subscribed to thumbs publication with one argument.
  • We created the thumbs helper that fetches all thumbnails of our saved files.
  • Mentioned before files variable. It contains identifiers of uploaded files.

Sort Images

We can also add an ability to sort and update the images order. To get the ability to sort the images, let's use angular-sortable-view.

$ meteor npm install angular-sortable-view --save

Let's add it as a dependency:

1
2
3
4
5
6
7
 
79
80
81
82
83
84
85
86
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
import 'angular-sortable-view';
import 'ng-img-crop/compile/minified/ng-img-crop';
import 'ng-img-crop/compile/minified/ng-img-crop.css';
 
...some lines skipped...
export default angular.module(name, [
  angularMeteor,
  ngFileUpload,
  'ngImgCrop',
  'angular-sortable-view'
]).component(name, {
  template,
  controllerAs: name,

Implement directives of angular-sortable-view:

20.36 Use directives on thumbnails imports/ui/components/partyUpload/partyUpload.html
29
30
31
32
33
34
35
36
37
      <img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
    </div>
  </div>
  <div layout="row" class="images-container-title" sv-root sv-part="partyUpload.thumbs">
    <div sv-element class="party-image-container" ng-class="{'main-image': $index === 0}" ng-repeat="thumb in partyUpload.thumbs">
      <img draggable="false" ng-src="{{ thumb.url }}"/>
    </div>
  </div>
</div>

You can now easily change their order.

Remember! First file is the main file which will be shown on parties list. We will implement it later.

Since we have sorting, croping and uploading. We can now add binding we prepared previously:

20.37 Define two-way data binding `files` imports/ui/components/partyUpload/partyUpload.js
83
84
85
86
87
88
89
90
91
  'angular-sortable-view'
]).component(name, {
  template,
  bindings: {
    files: '=?'
  },
  controllerAs: name,
  controller: PartyUpload
});
### Display party image on the list As always, we have to create component, let's call it PartyImage.
20.38 Create view for PartyImage imports/ui/components/partyImage/partyImage.html
1
<img ng-src="{{partyImage.mainImage.url}}"/>
20.39 Create PartyImage component imports/ui/components/partyImage/partyImage.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import template from './partyImage.html';
import { Images } from '../../../api/images';
 
class PartyImage {
  constructor($scope, $reactive) {
    'ngInject';
    $reactive(this).attach($scope);
 
    this.helpers({
      mainImage() {
        const images = this.getReactively('images', true);
        if (images) {
          return Images.findOne({
            _id: images[0]
          });
        }
      }
    });
  }
}
 
const name = 'partyImage';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  bindings: {
    images: '<'
  },
  controllerAs: name,
  controller: PartyImage
});

As you can see we defined images which is a one-way binding that tells to the PartyImage component which images have been uploaded to the party.

Objects of Images and Thumbs collections contains .url property with absolute url to the file.

Now let's implement it to the PartiesList by adding dependency and what is more important, by subscribing images. Subscription is needed to fetch defined images.

20.40 Implement PartyImage in PartiesList imports/ui/components/partiesList/partiesList.html
25
26
27
28
29
30
31
32
33
34
35
            </span>
            <span class="md-subhead">{{party.description}}</span>
          </md-card-title-text>
          <md-card-title-media ng-if="party.images">
            <div class="md-media-lg card-media">
              <party-image images="party.images"></party-image>
            </div>
          </md-card-title-media>
        </md-card-title>
        <md-card-content>
          <party-rsvps-list rsvps="party.rsvps"></party-rsvps-list>
20.41 Add PartyImage to PartiesList and subscribe to `images` imports/ui/components/partiesList/partiesList.js
14
15
16
17
18
19
20
 
37
38
39
40
41
42
43
 
83
84
85
86
87
88
89
90
import { name as PartyCreator } from '../partyCreator/partyCreator';
import { name as PartyRsvp } from '../partyRsvp/partyRsvp';
import { name as PartyRsvpsList } from '../partyRsvpsList/partyRsvpsList';
import { name as PartyImage } from '../partyImage/partyImage';
 
class PartiesList {
  constructor($scope, $reactive) {
...some lines skipped...
    ]);
 
    this.subscribe('users');
    this.subscribe('images');
 
    this.helpers({
      parties() {
...some lines skipped...
  PartyRemove,
  PartyCreator,
  PartyRsvp,
  PartyRsvpsList,
  PartyImage
]).component(name, {
  template,
  controllerAs: name,

And that's it!

Cloud Storage

By storing files in the cloud you can reduce your costs and get a lot of other benefits.

Since this chapter is all about uploading files and UploadFS doesn't have built-in support for cloud services we should mention another library for that.

We recommend you to use Slingshot. You can install it by running:

$ meteor add edgee:slingshot

It's very easy to use with AWS S3, Google Cloud and other cloud storage services.

From slignshot's repository:

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.