├── .gitignore ├── .versions ├── README.md ├── client └── views │ ├── display-image.html │ ├── display-image.js │ ├── upload-image.html │ └── upload-image.js ├── examples └── app │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ ├── README-example-app.md │ ├── image-app.css │ ├── image-app.html │ ├── image-app.js │ ├── packages │ └── image-upload │ └── settings-example.json ├── lib └── image_upload.js ├── package.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .build* 4 | /examples/*/settings.json 5 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | caching-compiler@1.0.0 9 | caching-html-compiler@1.0.2 10 | callback-hook@1.0.4 11 | cfs:access-point@0.1.49 12 | cfs:base-package@0.0.30 13 | cfs:collection@0.5.5 14 | cfs:collection-filters@0.2.4 15 | cfs:data-man@0.0.6 16 | cfs:file@0.1.17 17 | cfs:graphicsmagick@0.0.18 18 | cfs:gridfs@0.0.33 19 | cfs:http-methods@0.0.29 20 | cfs:http-publish@0.0.13 21 | cfs:power-queue@0.9.11 22 | cfs:reactive-list@0.0.9 23 | cfs:reactive-property@0.0.4 24 | cfs:s3@0.1.3 25 | cfs:standard-packages@0.5.9 26 | cfs:storage-adapter@0.2.2 27 | cfs:tempstore@0.1.5 28 | cfs:upload-http@0.0.20 29 | cfs:worker@0.1.4 30 | check@1.1.0 31 | ddp@1.2.2 32 | ddp-client@1.2.1 33 | ddp-common@1.2.2 34 | ddp-server@1.2.2 35 | deps@1.0.9 36 | diff-sequence@1.0.1 37 | ecmascript@0.1.6 38 | ecmascript-runtime@0.2.6 39 | ejson@1.0.7 40 | geojson-utils@1.0.4 41 | html-tools@1.0.5 42 | htmljs@1.0.5 43 | http@1.1.1 44 | id-map@1.0.4 45 | jquery@1.11.4 46 | livedata@1.0.15 47 | logging@1.0.8 48 | meteor@1.1.10 49 | minifiers@1.1.7 50 | minimongo@1.0.10 51 | mongo@1.1.3 52 | mongo-id@1.0.1 53 | mongo-livedata@1.0.9 54 | npm-mongo@1.4.39_1 55 | observe-sequence@1.0.7 56 | okgrow:image-upload@0.8.3 57 | ordered-dict@1.0.4 58 | promise@0.5.1 59 | raix:eventemitter@0.1.2 60 | random@1.0.5 61 | reactive-dict@1.1.3 62 | reactive-var@1.0.6 63 | retry@1.0.4 64 | routepolicy@1.0.6 65 | session@1.1.1 66 | spacebars@1.0.7 67 | spacebars-compiler@1.0.7 68 | templating@1.1.5 69 | templating-tools@1.0.0 70 | tracker@1.0.9 71 | ui@1.0.8 72 | underscore@1.0.4 73 | url@1.0.5 74 | webapp@1.2.3 75 | webapp-hashing@1.0.5 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | DISCLAIMER - ***Development of this package is currently on hiatus***. We are currently not actively developing this package due to both resource constraints and uncertainty about how well supported it will be in the future. We are using this in our active projects, so we will continue to do bug fixes as we encounter them, but feature requests may go unanswered. PRs are still welcome. 4 | 5 | # Image Upload for Meteor 6 | 7 | Image Upload makes it super easy for you to setup a photo input field along with all the trimmings (S3, collections, permissions, templates, etc.). Under the hood you will find ImageUpload is just a sugary wrapper around [collectionFS](https://atmospherejs.com/cfs). 8 | 9 | To get familiar with ImageUpload take a look at the rest of this read me as well as our example: 10 | 11 | [Example app live](http://ok-image-upload-demo.herokuapp.com/) 12 | 13 | [Example app source](https://github.com/okgrow/meteor-image-upload/blob/master/examples/app) 14 | 15 | ## One-time Setup 16 | 17 | ### Prerequisites 18 | 19 | 1. [AWS S3](http://aws.amazon.com/s3/) account for cloud file storage. 20 | 21 | 2. [GraphicsMagick](http://www.graphicsmagick.org/) or [ImageMagick](http://www.imagemagick.org/) on your local machine *and* deployment server for image manipulation. 22 | 23 | - **OS X:** `brew install imagemagick` or `brew install graphicsmagick` 24 | 25 | - **Modulus.io:** supports ImageMagick no setup needed 26 | 27 | - **Heroku, DigitalOcean, AWS EC2:** requires manual ImageMagick/GraphicsMagick installation. 28 | 29 | - **Meteor.com free hosting** does *not* support ImageMagick/GraphicsMagick, sorry no way around it :( 30 | 31 | 32 | ### Install & Configure 33 | 34 | Install from your terminal `meteor add okgrow:image-upload`. 35 | 36 | Configure in common code (*server* and *client* ). 37 | 38 | API: ` ImageUpload.configure( options ) ` 39 | 40 | Example 41 | ```javascript 42 | ImageUpload.configure({ 43 | accessKeyId: YOUR_ACCESS_KEY_ID, 44 | secretAccessKey: YOUR_SECRET_ACCESS_KEY, 45 | bucketName: YOUR_BUCKET_NAME, 46 | bucketUrl: YOUR_BUCKET_URL, //"https://your_bucket_name.s3.amazonaws.com/", 47 | maxUploadSize: 30 // MB 48 | }); 49 | ``` 50 | 51 | **WARNING** You should never store your keys publicly, instead use [Meteor.settings](http://docs.meteor.com/#/full/meteor_settings). Start your app using `meteor --settings settings.json`. Refer to our example app settings file to see how we do it. 52 | 53 | **Note** Since 0.8.0 `publicRead` is now set in the image collection options. ImageUpload will throw an error if you try and pass `publicRead` in `ImageUpload.configure()` 54 | 55 | ### Creating Image Collections 56 | 57 | The images you upload will be stored in separate *Image Upload* collections. You will probably have more than one Image Upload collection. The Image Upload collections are created differently from Meteor collections, we show you how to make these special collections below. 58 | 59 | Each Image Upload collection will reference and index it's documents to one of your app's data collections as specified, only one app data collection can be referenced per each Image Upload collection. We show you how to query by reference ids in [templating](#display-image) 60 | below. 61 | 62 | API: `ImageUpload.createCollection( name, reference, { [options] } )` 63 | 64 | Options: 65 | 66 | | Name | Optional | Description | 67 | | --- | :---: | --- | 68 | | **defaultPermissions** | optional | Enables default Allow rules on your image collection, see [Security Rules](#allowdeny-security-rules) to see the rules | 69 | | **publicRead** | optional | set to `true` to server files directly from S3, `bucketUrl` in `.configure()` is also required. Also allows visitors to view images if `defaultPermissions` is also true. | 70 | | **sizes** | optional | Let ImageMagick create multiple different sizes of each image automatically. Specify a size name as the key followed by an array for X,Y px lengths | 71 | 72 | The following creates an image collection called `userImages` which will be associated with the `Meteor.users` collection with images stored in four sizes: 73 | 74 | 1. The original image 75 | 2. "thumbnail": 200x200 px 76 | 3. "normal": 800x800 px 77 | 4. "large:": 1200x1200 px 78 | 79 | ```javascript 80 | UserImages = ImageUpload.createCollection("userImages", Meteor.users, { 81 | defaultPermissions: true, 82 | sizes: { 83 | thumbnail: [200, 200], 84 | normal: [800,800], 85 | large: [1200, 1200] 86 | } 87 | }); 88 | ``` 89 | 90 | 91 | ### Allow/Deny Security Rules 92 | 93 | Please add your own **allow/deny** rules and/or enable ImageUpload's `defaultPermissions` when creating the ImageUpload collection. 94 | 95 | defaultPermissions if enabled: 96 | ```javascript 97 | ImageCollection.allow({ 98 | insert: function(userId, doc) { 99 | // Any authenticated user can create images 100 | return !!userId; 101 | }, 102 | update: function(userId, doc) { 103 | // User can update their own image only 104 | return doc && doc.addedBy === userId; 105 | }, 106 | remove: function(userId, doc) { 107 | // User can remove their own image only 108 | return doc && doc.addedBy === userId; 109 | }, 110 | download: function (userId, fileObj) { 111 | // If publicRead has been set anyone can download, otherwise users 112 | // can only download images that they uploaded 113 | if (publicRead) { 114 | return true; 115 | } else { 116 | return fileObj.addedBy === userId; 117 | } 118 | } 119 | }); 120 | ``` 121 | 122 | *Note:* Since the image collection is based on `CollectionFS`, we use their `allow` 123 | and `deny` system. You can view their documentation here: 124 | 125 | https://github.com/CollectionFS/Meteor-CollectionFS#security 126 | 127 | 128 | ## Client-side Templating 129 | 130 | ### Upload Image Template 131 | 132 | You want a nice upload button with everything wired up for you? We got ya covered. 133 | 134 | API: `{{> uploadImage imageCollection=collectionName [option=option] }}` 135 | 136 | Examples: 137 | ```html 138 | {{> uploadImage imageCollection="userImages" size="thumbnail" doc=currentUser classImage="tiny-img round"}} 139 | 140 | {{> uploadImage imageCollection="postImages" name="post-image" size="banner" }} 141 | ``` 142 | 143 | Attributes: 144 | 145 | | Name | Optional | Description | 146 | | --- | :---: | --- | 147 | | **imageCollection** | required | Specify the Image Upload collection images go to. *hint: This was the first parameter when creating the Image Upload collection.* | 148 | | **doc** | optional | When adding a new image to an existing document you can pass the existing document and we will make the reference for you. We pull the reference `_id` from the supplied object. | 149 | | **size** | optional | Specify the image size you want displayed when upload completes. By default this partial template displays the original uploaded image once complete. *hint: You made these sizes when creating your Image Upload collection.* | 150 | | **name** | optional | Specify a custom input element name. This overwrites the default input name attribute, `image` | 151 | | **classInput** | optional | Specify custom CSS class(es) for the input element. Included class is `image-file-picker` | 152 | | **classImage** | optional | Specify custom CSS class(es) for the image when it displays after upload completes. Included class is`uploaded-image` | 153 | 154 | 155 | ### Display Image 156 | 157 | To display a stored image, you can 158 | 159 | ```html 160 | 163 | ``` 164 | 165 | Which uses a helper which loads a document from the image collection: 166 | 167 | ```javascript 168 | Template.yourTemplate.helpers({ 169 | image: function() { 170 | var doc = Template.parentData(1); 171 | var image = yourImageCollection.findOne({associatedObjectId: doc._id}); 172 | if (image) { 173 | return image.url({store: "userImages-thumbnail"}); 174 | } 175 | } 176 | }); 177 | ``` 178 | 179 | ## Roadmap / TODO 180 | 181 | In order of fuzzy priority: 182 | 183 | - Less configuration to setup 184 | - Better error handling 185 | - Default upload progress bar 186 | - In-browser image cropping/resizing with darkroom.js 187 | - Upload files from a URL 188 | 189 | ### Contributing 190 | 191 | Issues and Pull Requests are always welcome. Please read our [contribution guidelines](https://github.com/okgrow/guides/blob/master/contributing.md). 192 | 193 | ### Direct Uploads to S3? 194 | 195 | At this point we don't have plans to support uploading files directly from 196 | the browser's client to AWS's S3. We may add this in the future, but there we 197 | will probably wait until CollectionFS supports this (they have it in the 198 | works). Pull requests welcome. 199 | 200 | 201 | Enjoy! 202 | -------------------------------------------------------------------------------- /client/views/display-image.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /client/views/display-image.js: -------------------------------------------------------------------------------- 1 | Template.displayImage.helpers({ 2 | thumbnail: function () { 3 | var doc = Template.parentData(1); 4 | var store = doc.imageCollection + "-original"; 5 | if(doc.size){ 6 | store = doc.imageCollection + "-" + doc.size; 7 | } 8 | return this.url({store: store}); 9 | } 10 | }); 11 | 12 | Template.displayImage.events({ 13 | "click [data-action=delete-image]": function () { 14 | this.collection.remove(this._id); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/views/upload-image.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/views/upload-image.js: -------------------------------------------------------------------------------- 1 | Template.uploadImage.helpers({ 2 | image: function() { 3 | var self = this; 4 | var coll = ImageUpload.getImageCollection(self.imageCollection); 5 | var store = self.imageCollection; 6 | var image; 7 | 8 | if (self.doc) { 9 | // Look for image for associated object 10 | image = coll.findOne({associatedObjectId: self.doc._id}); 11 | } else { 12 | // No associated object yet, check id of last image of this type in session 13 | imageId = Session.get("lastImageId-" + store); 14 | image = coll.findOne({_id: imageId}); 15 | } 16 | return image; 17 | }, 18 | inputName: function(){ 19 | return this.name || "image"; 20 | } 21 | }); 22 | 23 | Template.uploadImage.events({ 24 | "change [data-action=image-file-picker]": function(event, template) { 25 | var self = this; 26 | var file = event.target.files[0]; 27 | var coll = ImageUpload.getImageCollection(self.imageCollection); 28 | if (file) { 29 | var newFile = new FS.File(file); 30 | newFile.addedBy = Meteor.userId(); 31 | if (self.doc) { 32 | newFile.associatedObjectId = self.doc._id; 33 | } 34 | coll.insert(newFile, function (err, fileObj) { 35 | if (err) { 36 | console.log("Error: ", err); 37 | } 38 | // Inserted new doc with ID fileObj._id, and kicked off the data upload using HTTP 39 | if (!self.associatedObjectId) { 40 | // Save the ID of the newly inserted doc in the session so we can use it 41 | // until it's associated. 42 | Session.set("lastImageId-" + self.imageCollection, fileObj._id); 43 | } 44 | }); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /examples/app/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/app/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 19o0n6i1lytv8dahu0r 8 | -------------------------------------------------------------------------------- /examples/app/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | accounts-ui 8 | okgrow:image-upload 9 | pauldowman:dotenv 10 | accounts-meteor-developer 11 | twbs:bootstrap 12 | meteor-base 13 | mobile-experience 14 | mongo 15 | blaze-html-templates 16 | session 17 | jquery 18 | tracker 19 | logging 20 | reload 21 | random 22 | ejson 23 | spacebars 24 | check 25 | standard-minifier-css 26 | standard-minifier-js 27 | -------------------------------------------------------------------------------- /examples/app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.2.4 2 | -------------------------------------------------------------------------------- /examples/app/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.7 2 | accounts-meteor-developer@1.0.9 3 | accounts-oauth@1.1.12 4 | accounts-ui@1.1.9 5 | accounts-ui-unstyled@1.1.12 6 | allow-deny@1.0.4 7 | autoupdate@1.2.9 8 | babel-compiler@6.6.4 9 | babel-runtime@0.1.8 10 | base64@1.0.8 11 | binary-heap@1.0.8 12 | blaze@2.1.7 13 | blaze-html-templates@1.0.4 14 | blaze-tools@1.0.8 15 | boilerplate-generator@1.0.8 16 | caching-compiler@1.0.4 17 | caching-html-compiler@1.0.6 18 | callback-hook@1.0.8 19 | cfs:access-point@0.1.49 20 | cfs:base-package@0.0.30 21 | cfs:collection@0.5.5 22 | cfs:collection-filters@0.2.4 23 | cfs:data-man@0.0.6 24 | cfs:file@0.1.17 25 | cfs:graphicsmagick@0.0.18 26 | cfs:gridfs@0.0.33 27 | cfs:http-methods@0.0.32 28 | cfs:http-publish@0.0.13 29 | cfs:power-queue@0.9.11 30 | cfs:reactive-list@0.0.9 31 | cfs:reactive-property@0.0.4 32 | cfs:s3@0.1.3 33 | cfs:standard-packages@0.5.9 34 | cfs:storage-adapter@0.2.3 35 | cfs:tempstore@0.1.5 36 | cfs:upload-http@0.0.20 37 | cfs:worker@0.1.4 38 | check@1.2.1 39 | ddp@1.2.5 40 | ddp-client@1.2.7 41 | ddp-common@1.2.5 42 | ddp-rate-limiter@1.0.4 43 | ddp-server@1.2.6 44 | deps@1.0.12 45 | diff-sequence@1.0.5 46 | ecmascript@0.4.3 47 | ecmascript-runtime@0.2.10 48 | ejson@1.0.11 49 | fastclick@1.0.11 50 | geojson-utils@1.0.8 51 | hot-code-push@1.0.4 52 | html-tools@1.0.9 53 | htmljs@1.0.9 54 | http@1.1.5 55 | id-map@1.0.7 56 | jquery@1.11.8 57 | launch-screen@1.0.11 58 | less@2.6.0 59 | livedata@1.0.18 60 | localstorage@1.0.9 61 | logging@1.0.12 62 | meteor@1.1.14 63 | meteor-base@1.0.4 64 | meteor-developer@1.1.9 65 | minifier-css@1.1.11 66 | minifier-js@1.1.11 67 | minimongo@1.0.16 68 | mobile-experience@1.0.4 69 | mobile-status-bar@1.0.12 70 | modules@0.6.1 71 | modules-runtime@0.6.3 72 | mongo@1.1.7 73 | mongo-id@1.0.4 74 | mongo-livedata@1.0.12 75 | npm-mongo@1.4.43 76 | oauth@1.1.10 77 | oauth2@1.1.9 78 | observe-sequence@1.0.11 79 | okgrow:image-upload@0.8.3 80 | ordered-dict@1.0.7 81 | pauldowman:dotenv@1.0.1 82 | promise@0.6.7 83 | raix:eventemitter@0.1.3 84 | random@1.0.9 85 | rate-limit@1.0.4 86 | reactive-dict@1.1.7 87 | reactive-var@1.0.9 88 | reload@1.1.8 89 | retry@1.0.7 90 | routepolicy@1.0.10 91 | service-configuration@1.0.9 92 | session@1.1.5 93 | spacebars@1.0.11 94 | spacebars-compiler@1.0.11 95 | standard-minifier-css@1.0.6 96 | standard-minifier-js@1.0.6 97 | templating@1.1.9 98 | templating-tools@1.0.4 99 | tracker@1.0.13 100 | twbs:bootstrap@3.3.6 101 | ui@1.0.11 102 | underscore@1.0.8 103 | url@1.0.9 104 | webapp@1.2.8 105 | webapp-hashing@1.0.9 106 | -------------------------------------------------------------------------------- /examples/app/README-example-app.md: -------------------------------------------------------------------------------- 1 | Image Upload Demo App 2 | --------------------- 3 | 4 | Note that the demo app is purged of data on the hour every hour. 5 | 6 | Running this app yourself 7 | ------------------------- 8 | 9 | You will need GraphicsMagick installed. 10 | 11 | To install on OS X: `brew install graphicsmagick` 12 | 13 | Copy `settings-example.json` to `settings.json` and edit with your actual 14 | AWS credentials and bucket name. 15 | 16 | Start the app: `meteor --settings settings.json` 17 | 18 | Deploying this app 19 | ------------------ 20 | 21 | Use Heroku. You can't use Meteor.com's deployment environment because it does 22 | not support GraphicsMagick. (We have a [branch](https://github.com/okgrow/meteor-image-upload/tree/heroku-deploy) 23 | that contains the example app at the root for deploying to Heroku, we only merge 24 | one way from master to that branch.) 25 | -------------------------------------------------------------------------------- /examples/app/image-app.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | -------------------------------------------------------------------------------- /examples/app/image-app.html: -------------------------------------------------------------------------------- 1 | 2 | Image Upload Demo 3 | 4 | 5 | 6 |
7 |
8 |
9 | {{> loginButtons}} 10 |
11 |
12 |
13 |
14 | {{> users}} 15 |
16 |

Note that all data (users, posts, images) are purged periodically. If everything disappears while you're viewing this, that's what has happened.

17 |

You can view this app's source code.

18 |
19 | {{#if currentUser}} 20 | {{> profile}} 21 |
22 | {{> Template.dynamic template=formTemplate data=formData}} 23 |
24 | {{else}} 25 | Sign in to send a chat message 26 | {{/if}} 27 | {{> posts}} 28 |
29 |
30 |
31 | 32 | 33 | 39 | 40 | 50 | 51 | 59 | 60 | 73 | 74 | 88 | 89 | 99 | 100 | 115 | -------------------------------------------------------------------------------- /examples/app/image-app.js: -------------------------------------------------------------------------------- 1 | var bucketUrl = Meteor.settings.public.bucketUrl; 2 | 3 | Posts = new Mongo.Collection("posts"); 4 | 5 | if (Meteor.isServer) { 6 | var accessKeyId = Meteor.settings.accessKeyId; 7 | var secretAccessKey = Meteor.settings.secretAccessKey; 8 | var bucketName = Meteor.settings.bucketName; 9 | 10 | Meteor.publish("users", function() { 11 | return Meteor.users.find({}); 12 | }); 13 | 14 | Meteor.publish("posts", function() { 15 | return Posts.find({}); 16 | }); 17 | 18 | } 19 | 20 | ImageUpload.configure({ 21 | accessKeyId: accessKeyId, 22 | secretAccessKey: secretAccessKey, 23 | bucketName: bucketName, 24 | bucketUrl: bucketUrl 25 | }); 26 | 27 | UserImages = ImageUpload.createCollection("userImages", Meteor.users, { 28 | defaultPermissions: true, 29 | publicRead: true, 30 | sizes: { 31 | normal: [800,800], 32 | thumbnail: [200, 200], 33 | avatar: [50, 50] 34 | } 35 | }); 36 | 37 | PostImages = ImageUpload.createCollection("postImages", Posts, { 38 | defaultPermissions: true, 39 | sizes: { 40 | normal: [300,300], 41 | thumbnail: [100, 100], 42 | avatar: [50, 50] 43 | } 44 | }); 45 | 46 | if (Meteor.isServer) { 47 | Posts.allow({ 48 | insert: function(userId, text) { 49 | return !!userId; 50 | }, 51 | update: function(userId, doc) { 52 | /* 53 | * Posts can't be edited after they're created 54 | */ 55 | return doc && doc.user._id === userId; 56 | } 57 | }); 58 | 59 | UserImages.allow({ 60 | insert: function(userId, doc) { 61 | return !!userId; 62 | }, 63 | update: function(userId, doc) { 64 | /* 65 | * User can update their own image only 66 | */ 67 | return doc && doc.addedBy === userId; 68 | }, 69 | remove: function(userId, doc) { 70 | /* 71 | * User can remove their own image only 72 | */ 73 | return doc && doc.addedBy === userId; 74 | }, 75 | download: function(userId, fsFile) { 76 | /* 77 | * Anyone can see a user's avatar 78 | */ 79 | return true; 80 | } 81 | }); 82 | 83 | PostImages.allow({ 84 | insert: function(userId, doc) { 85 | return true; 86 | }, 87 | update: function(userId, doc) { 88 | /* 89 | * Creator can update. Not that this is required because a PostImage 90 | * is created immediately after the upload is completed, but before 91 | * an associated Post document is created. In order to create the 92 | * association, the user must be able to update their message. There 93 | * are work arounds that go beyond the scope of this demo. 94 | */ 95 | return doc && doc.addedBy === userId; 96 | }, 97 | remove: function(userId, doc) { 98 | /* 99 | * Can't be deleted, ever 100 | */ 101 | return false; 102 | }, 103 | download: function(userId, fsFile) { 104 | /* 105 | * Must be signed in 106 | * Note that if `publicRead` is set for ImageUpload, download is 107 | * implicitly permitted, even if this function would return false. 108 | */ 109 | return userId; 110 | } 111 | }); 112 | 113 | } 114 | 115 | if (Meteor.isClient) { 116 | 117 | Meteor.subscribe("posts"); 118 | Meteor.subscribe("users"); 119 | Session.set("formTemplate", "makeAPost"); 120 | Session.set("formData", undefined); 121 | Session.set("lastImageId-postImages", undefined); 122 | 123 | Template.body.helpers({ 124 | formTemplate: function () { 125 | return Session.get("formTemplate"); 126 | }, 127 | formData: function () { 128 | return Session.get("formData"); 129 | } 130 | }); 131 | 132 | Template.profile.helpers({ 133 | imageUploadConfig: function() { 134 | return { 135 | imageCollection: UserImages, 136 | store: "userImages-thumbnail" 137 | }; 138 | }, 139 | coll: function() { 140 | return UserImages; 141 | }, 142 | handle: function() { 143 | return Meteor.user().profile.name; 144 | } 145 | }); 146 | 147 | Template.users.helpers({ 148 | users: function() { 149 | return Meteor.users.find({}); 150 | } 151 | }); 152 | 153 | Template.user.helpers({ 154 | id: function() { 155 | return this.profile.name; 156 | }, 157 | avatar: function() { 158 | return UserImages.findOne({associatedObjectId: this._id}); 159 | }, 160 | imageUrl: function() { 161 | return UserImages.findOne({associatedObjectId: this._id}).url({store: "userImages-avatar"}); 162 | } 163 | }); 164 | 165 | Template.posts.helpers({ 166 | posts: function() { 167 | return Posts.find({}); 168 | } 169 | }); 170 | 171 | Template.post.helpers({ 172 | text: function() { 173 | return this.text; 174 | }, 175 | user: function() { 176 | return this.user.profile.name; 177 | }, 178 | image: function() { 179 | var image = PostImages.findOne({associatedObjectId: this._id}); 180 | if (image) { 181 | return image.url({store: "postImages-thumbnail"}); 182 | } 183 | } 184 | }); 185 | 186 | Template.post.events({ 187 | "click [data-action=post-edit]": function (event) { 188 | var image = PostImages.findOne({associatedObjectId:this._id}); 189 | Session.set("formTemplate", "editAPost"); 190 | Session.set("formData", this); 191 | if (image && image._id) { 192 | Session.set("lastImageId-postImages", image._id); 193 | } 194 | } 195 | }); 196 | 197 | Template.makeAPost.events({ 198 | "submit [data-action=submit-post]": function(event) { 199 | event.preventDefault(); 200 | var text = event.target.text.value; 201 | if (!Meteor.user()) { 202 | return false; 203 | } 204 | var imgId = Session.get("lastImageId-postImages"); 205 | Session.set("lastImageId-postImages", undefined); 206 | Posts.insert({ 207 | text: text, 208 | user: Meteor.user() 209 | }, 210 | function(error, postId) { 211 | if (error) { 212 | throw new Meteor.Error(error); 213 | } 214 | PostImages.update(imgId, {$set: { associatedObjectId: postId }}); 215 | } 216 | ); 217 | $("form[data-action=submit-post]").get(0).reset(); 218 | return false; 219 | } 220 | }); 221 | 222 | Template.editAPost.events({ 223 | "click [data-action=cancel-edit-post]": function () { 224 | $("form[data-action=edit-post]").get(0).reset(); 225 | Session.set("formTemplate", "makeAPost"); 226 | Session.set("lastImageId-postImages", undefined); 227 | }, 228 | "submit [data-action=edit-post]": function(event) { 229 | event.preventDefault(); 230 | var text = event.target.text.value; 231 | if (!Meteor.user()) { 232 | return false; 233 | } 234 | var imgId = Session.get("lastImageId-postImages"); 235 | Session.set("lastImageId-postImages", undefined); 236 | Posts.update({_id: this._id},{ $set: { 237 | text: text, 238 | user: Meteor.user() 239 | } 240 | }, function () { 241 | Session.set("formTemplate", "makeAPost"); 242 | } 243 | ); 244 | $("form[data-action=edit-post]").get(0).reset(); 245 | return false; 246 | } 247 | }); 248 | } 249 | 250 | if (Meteor.isServer) { 251 | /* 252 | * Purge data every hour 253 | */ 254 | Meteor.setInterval(function purge() { 255 | PostImages.remove({}); 256 | UserImages.remove({}); 257 | Posts.remove({}); 258 | Meteor.users.remove({}); 259 | }, 1000 * 60 * 60); 260 | } 261 | -------------------------------------------------------------------------------- /examples/app/packages/image-upload: -------------------------------------------------------------------------------- 1 | ../../../ -------------------------------------------------------------------------------- /examples/app/settings-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessKeyId": "ABCDEFGHIJKLMNOP", 3 | "secretAccessKey": "vlkdfoijfoncos**fake**kdnflkndklnlnvlknfdv", 4 | "bucketName": "your-bucket", 5 | "public": { 6 | "bucketUrl": "https://s3.amazonaws.com/your-bucket" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/image_upload.js: -------------------------------------------------------------------------------- 1 | ImageUpload = (function ImageUploadClosure() { 2 | var obj = {}; 3 | obj.collections = {}; 4 | var MAX_UPLOAD_SIZE = 20; // 20 MB 5 | var accessKeyId, secretAccessKey, bucketName; 6 | var bucketUrl, maxUploadSize; 7 | 8 | //Set Cache Control headers so we don't overload our meteor server with http requests 9 | FS.HTTP.setHeadersForGet([['Cache-Control', 'public, max-age=31536000']]); 10 | 11 | function postConfigure() { 12 | if (bucketUrl) { 13 | // strip trailing slash so we only have one case when we generate 14 | // public file URLs 15 | bucketUrl = bucketUrl.replace(/\/$/, ''); 16 | } 17 | setupPublicRead(); 18 | } 19 | 20 | function setupPublicRead() { 21 | // Save the old url method 22 | FS.File.prototype._url = FS.File.prototype.url; 23 | 24 | // New direct-to-S3 url method 25 | FS.File.prototype.url = function(options) { 26 | var self = this; 27 | var store = options && options.store; 28 | var primaryStore = self.getCollection().primaryStore; 29 | 30 | // Use the old url() method to reactively show S3 URL only when file is stored and publicRead is true. 31 | // TODO: figure out a less hacky way. Use hasStored()? 32 | if (primaryStore.publicRead && bucketUrl) { 33 | if (self._url(options)) { 34 | var fileKey = store + '/' + self.collectionName + '/' + self._id + '-' + self.name(); 35 | return bucketUrl + '/' + fileKey; 36 | } 37 | return null; 38 | } else if (primaryStore.defaultPermissions && !Meteor.userId()) { 39 | return null; 40 | } else { 41 | // if publicRead is not true then use old url() as normal 42 | return self._url(options); 43 | } 44 | }; 45 | } 46 | 47 | /* 48 | * Automatically set up publish/subscribe for the client for their 49 | * image collection. 50 | * 51 | * imageCollection: an FS.Collection object which holds our file metadata 52 | * baseCollection: a Meteor.Collection object which is referenced by `imageCollection` 53 | * imageCollectionName: a String name of the image collection, used as pub/sub key 54 | */ 55 | function setupClientSubscription(imageCollection, baseCollection, imageCollectionName) { 56 | if (Meteor.isServer) { 57 | Meteor.publish("associated-images-" + imageCollectionName, function(associatedObjectIds) { 58 | check(associatedObjectIds, Match.OneOf( 59 | Match.Optional([String]), 60 | Match.Optional([Meteor.Collection.ObjectID]) 61 | )); 62 | if (associatedObjectIds === undefined || associatedObjectIds === null || associatedObjectIds.length === 0) { 63 | return; 64 | } 65 | return imageCollection.find({ associatedObjectId: { $in: associatedObjectIds } }); 66 | }); 67 | Meteor.publish("single-image-" + imageCollectionName, function(imageId) { 68 | check(imageId, Match.Where(function (id) { 69 | if (id === null) { 70 | check(id, null); 71 | return true; 72 | } else { 73 | check(id, String); 74 | return true; 75 | } 76 | })); 77 | return imageCollection.find({ _id: imageId }); 78 | }); 79 | } 80 | if (Meteor.isClient) { 81 | Tracker.autorun(function() { 82 | var associatedObjectIds = baseCollection.find({}, {fields: { _id: 1 }}).map(function(doc) { return doc._id; }); 83 | Meteor.subscribe("associated-images-" + imageCollectionName, associatedObjectIds); 84 | Meteor.subscribe("single-image-" + imageCollectionName, Session.get("lastImageId-" + imageCollectionName)); 85 | }); 86 | } 87 | } 88 | 89 | obj.getImageCollection = function getImageCollection(imageCollectionName){ 90 | var coll = ImageUpload.collections[imageCollectionName]; 91 | if(coll){ 92 | return coll; 93 | } else { 94 | throw new Error("Cannot find image collection with name "+imageCollectionName+"."); 95 | } 96 | }; 97 | 98 | obj.configure = function configure(options) { 99 | if (bucketName !== undefined) { 100 | console.warn("ImageUpload.configure() has previously been called!"); 101 | } 102 | if ( _.has(options,'publicRead') ) { 103 | throw new Error("ImageUpload no longer supports publicRead in ImageUpload.configure(). publicRead is now set in the image collection options."); 104 | } 105 | accessKeyId = options.accessKeyId; 106 | secretAccessKey = options.secretAccessKey; 107 | bucketName = options.bucketName; 108 | bucketUrl = options.bucketUrl; 109 | maxUploadSize = options.maxUploadSize; 110 | postConfigure(); 111 | }; 112 | 113 | // sizes looks like {thumbnail: [100, 100], normal: [200,300]} 114 | // there will also be one named "original" containing the original image at 115 | // full size 116 | obj.createCollection = function createCollection(collectionName, baseCollection, options) { 117 | var self = this; 118 | var acl = options.publicRead ? 'public-read' : 'private'; 119 | if (Meteor.isServer && bucketName === undefined) { 120 | console.warn("ImageUpload.configure() has not yet been called!"); 121 | } 122 | var stores = []; 123 | 124 | stores.push( 125 | new FS.Store.S3(collectionName + "-original", { 126 | bucket: bucketName, 127 | folder: collectionName + "-original", 128 | ACL: acl, 129 | publicRead: options.publicRead, 130 | defaultPermissions: options.defaultPermissions, 131 | accessKeyId: accessKeyId, 132 | secretAccessKey: secretAccessKey 133 | }) 134 | ); 135 | 136 | _.each(options.sizes, function (dimensions, sizeName) { 137 | var x, y, store; 138 | 139 | x = dimensions[0]; 140 | y = dimensions[1]; 141 | 142 | store = new FS.Store.S3 ((collectionName + "-" + sizeName), { 143 | bucket: bucketName, //required 144 | folder: collectionName + "-" + sizeName, 145 | ACL: acl, 146 | accessKeyId: accessKeyId, 147 | secretAccessKey: secretAccessKey, 148 | 149 | //Create the thumbnail as we save to the store. 150 | transformWrite: function (fileObj, readStream, writeStream) { 151 | /* Use graphicsmagick to create a XXxYY square thumbnail at 100% quality, 152 | * orient according to EXIF data if necessary and then save by piping to the 153 | * provided writeStream */ 154 | if (gm.isAvailable) { 155 | gm(readStream, fileObj.name) 156 | .resize(x,y,"^") 157 | .gravity('Center'). 158 | crop(x, y). 159 | quality(100). 160 | autoOrient(). 161 | stream().pipe(writeStream); 162 | } else { 163 | console.warn("GraphicsMagick/ImageMagick not available"); 164 | } 165 | } 166 | }); 167 | stores.push(store); 168 | }); 169 | 170 | self.collections[collectionName] = new FS.Collection (collectionName, { 171 | stores: stores, 172 | filter: { 173 | maxSize: (maxUploadSize || MAX_UPLOAD_SIZE) * 1024 * 1024, 174 | allow: { 175 | contentTypes: ['image/*'], 176 | extensions: ['png', 'jpg', 'jpeg', 'gif'] 177 | }, 178 | onInvalid: function (message) { 179 | if(Meteor.isClient){ 180 | alert(message); 181 | }else{ 182 | console.warn(message); 183 | } 184 | } 185 | } 186 | }); 187 | 188 | // =============================================== 189 | // Default Permissions for Image Collections 190 | // =============================================== 191 | // 192 | // Default permissions: user images are viewable by that user only, 193 | // all other images require to be logged in. 194 | if(options.defaultPermissions) { 195 | self.collections[collectionName].allow({ 196 | insert: function(userId) { 197 | return !!userId; 198 | }, 199 | update: function(userId, doc) { 200 | /* 201 | * User can update their own image only 202 | */ 203 | return doc && doc.addedBy === userId; 204 | }, 205 | remove: function(userId, doc) { 206 | /* 207 | * User can remove their own image only 208 | */ 209 | return doc && doc.addedBy === userId; 210 | }, 211 | download: function (userId, fileObj) { 212 | if (options.publicRead) { 213 | return true; 214 | } else { 215 | return fileObj.addedBy === userId; 216 | } 217 | } 218 | }); 219 | } 220 | 221 | self.collections[collectionName].on('error', function(error, fileObj) { 222 | /* 223 | * TODO: Better error handling than just console logging 224 | */ 225 | console.error(error.message); 226 | fileObj.on('error', function() { 227 | /* 228 | * fileObj.emit doesn't pass useful information here, so do nothing. 229 | * Errors should have been handled by the store's error handler. 230 | * https://github.com/CollectionFS/Meteor-cfs-collection/blob/64a0fc6e6a95468c5ff8dc9e81a9c56ecf9aeb6a/common.js#L94 231 | */ 232 | }); 233 | }); 234 | 235 | if (Meteor.isServer) { 236 | self.collections[collectionName].files._ensureIndex({associatedObjectId: 1}); 237 | } 238 | 239 | setupClientSubscription(self.collections[collectionName], baseCollection, collectionName); 240 | 241 | return self.collections[collectionName]; 242 | }; 243 | 244 | return obj; 245 | })(); 246 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "okgrow:image-upload", 3 | version: "0.8.3", 4 | summary: "Let users upload images in your app", 5 | git: "https://github.com/okgrow/meteor-image-upload/" 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.use([ 10 | 'cfs:standard-packages@0.5.9', 11 | 'cfs:gridfs@0.0.33', 12 | 'cfs:graphicsmagick@0.0.18', 13 | 'cfs:s3@0.1.3', 14 | 'cfs:tempstore@0.1.5', 15 | 'cfs:power-queue@0.9.11', 16 | 'session@1.0.5', 17 | 'spacebars@1.0.0', 18 | 'templating@1.0.0', 19 | 'underscore@1.0.4', 20 | 'tracker@1.0.8' 21 | ]); 22 | api.addFiles('lib/image_upload.js', ['server', 'client']); 23 | api.addFiles( 24 | [ 25 | 'client/views/display-image.html', 26 | 'client/views/display-image.js', 27 | 'client/views/upload-image.html', 28 | 'client/views/upload-image.js' 29 | ], 30 | 'client'); 31 | api.export('ImageUpload'); 32 | }); 33 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "application-configuration", 5 | "1.0.3" 6 | ], 7 | [ 8 | "base64", 9 | "1.0.1" 10 | ], 11 | [ 12 | "binary-heap", 13 | "1.0.1" 14 | ], 15 | [ 16 | "blaze", 17 | "2.0.3" 18 | ], 19 | [ 20 | "blaze-tools", 21 | "1.0.1" 22 | ], 23 | [ 24 | "boilerplate-generator", 25 | "1.0.1" 26 | ], 27 | [ 28 | "callback-hook", 29 | "1.0.1" 30 | ], 31 | [ 32 | "cfs:access-point", 33 | "0.1.43" 34 | ], 35 | [ 36 | "cfs:base-package", 37 | "0.0.27" 38 | ], 39 | [ 40 | "cfs:collection", 41 | "0.5.3" 42 | ], 43 | [ 44 | "cfs:collection-filters", 45 | "0.2.3" 46 | ], 47 | [ 48 | "cfs:data-man", 49 | "0.0.4" 50 | ], 51 | [ 52 | "cfs:file", 53 | "0.1.15" 54 | ], 55 | [ 56 | "cfs:filesystem", 57 | "0.1.1" 58 | ], 59 | [ 60 | "cfs:graphicsmagick", 61 | "0.0.17" 62 | ], 63 | [ 64 | "cfs:http-methods", 65 | "0.0.27" 66 | ], 67 | [ 68 | "cfs:http-publish", 69 | "0.0.13" 70 | ], 71 | [ 72 | "cfs:power-queue", 73 | "0.9.11" 74 | ], 75 | [ 76 | "cfs:reactive-list", 77 | "0.0.9" 78 | ], 79 | [ 80 | "cfs:reactive-property", 81 | "0.0.4" 82 | ], 83 | [ 84 | "cfs:s3", 85 | "0.1.1" 86 | ], 87 | [ 88 | "cfs:standard-packages", 89 | "0.5.3" 90 | ], 91 | [ 92 | "cfs:storage-adapter", 93 | "0.1.1" 94 | ], 95 | [ 96 | "cfs:tempstore", 97 | "0.1.3" 98 | ], 99 | [ 100 | "cfs:upload-http", 101 | "0.0.19" 102 | ], 103 | [ 104 | "cfs:worker", 105 | "0.1.3" 106 | ], 107 | [ 108 | "check", 109 | "1.0.2" 110 | ], 111 | [ 112 | "ddp", 113 | "1.0.12" 114 | ], 115 | [ 116 | "deps", 117 | "1.0.5" 118 | ], 119 | [ 120 | "ejson", 121 | "1.0.4" 122 | ], 123 | [ 124 | "follower-livedata", 125 | "1.0.2" 126 | ], 127 | [ 128 | "geojson-utils", 129 | "1.0.1" 130 | ], 131 | [ 132 | "html-tools", 133 | "1.0.2" 134 | ], 135 | [ 136 | "htmljs", 137 | "1.0.2" 138 | ], 139 | [ 140 | "http", 141 | "1.0.8" 142 | ], 143 | [ 144 | "id-map", 145 | "1.0.1" 146 | ], 147 | [ 148 | "jquery", 149 | "1.0.1" 150 | ], 151 | [ 152 | "json", 153 | "1.0.1" 154 | ], 155 | [ 156 | "livedata", 157 | "1.0.11" 158 | ], 159 | [ 160 | "logging", 161 | "1.0.5" 162 | ], 163 | [ 164 | "meteor", 165 | "1.1.3" 166 | ], 167 | [ 168 | "minifiers", 169 | "1.1.2" 170 | ], 171 | [ 172 | "minimongo", 173 | "1.0.5" 174 | ], 175 | [ 176 | "mongo", 177 | "1.0.9" 178 | ], 179 | [ 180 | "mongo-livedata", 181 | "1.0.6" 182 | ], 183 | [ 184 | "observe-sequence", 185 | "1.0.3" 186 | ], 187 | [ 188 | "ordered-dict", 189 | "1.0.1" 190 | ], 191 | [ 192 | "raix:eventemitter", 193 | "0.1.1" 194 | ], 195 | [ 196 | "random", 197 | "1.0.1" 198 | ], 199 | [ 200 | "reactive-dict", 201 | "1.0.4" 202 | ], 203 | [ 204 | "reactive-var", 205 | "1.0.3" 206 | ], 207 | [ 208 | "retry", 209 | "1.0.1" 210 | ], 211 | [ 212 | "routepolicy", 213 | "1.0.2" 214 | ], 215 | [ 216 | "session", 217 | "1.0.4" 218 | ], 219 | [ 220 | "spacebars", 221 | "1.0.3" 222 | ], 223 | [ 224 | "spacebars-compiler", 225 | "1.0.3" 226 | ], 227 | [ 228 | "templating", 229 | "1.0.9" 230 | ], 231 | [ 232 | "tracker", 233 | "1.0.3" 234 | ], 235 | [ 236 | "ui", 237 | "1.0.4" 238 | ], 239 | [ 240 | "underscore", 241 | "1.0.1" 242 | ], 243 | [ 244 | "url", 245 | "1.0.2" 246 | ], 247 | [ 248 | "webapp", 249 | "1.1.4" 250 | ], 251 | [ 252 | "webapp-hashing", 253 | "1.0.1" 254 | ] 255 | ], 256 | "pluginDependencies": [], 257 | "toolVersion": "meteor-tool@1.0.36", 258 | "format": "1.0" 259 | } --------------------------------------------------------------------------------