stratosphere 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +262 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/javascripts/stratosphere/inflection.min.js +22 -0
  6. data/app/assets/javascripts/stratosphere/rsvp.min.js +9 -0
  7. data/app/assets/javascripts/stratosphere/stratosphere.bundled.min.js +31 -0
  8. data/app/assets/javascripts/stratosphere/stratosphere.js +297 -0
  9. data/app/controllers/stratosphere/application_controller.rb +4 -0
  10. data/app/helpers/stratosphere/application_helper.rb +4 -0
  11. data/app/vendor/assets/javascripts/rsvp/README.md +9 -0
  12. data/app/vendor/assets/javascripts/rsvp/bower.json +22 -0
  13. data/app/vendor/assets/javascripts/rsvp/composer.json +18 -0
  14. data/app/vendor/assets/javascripts/rsvp/package.json +59 -0
  15. data/app/vendor/assets/javascripts/rsvp/rsvp.js +1671 -0
  16. data/app/vendor/assets/javascripts/rsvp/rsvp.min.js +9 -0
  17. data/app/views/_attachment_field.html.erb +42 -0
  18. data/config/initializers/action_controller.rb +56 -0
  19. data/config/routes.rb +2 -0
  20. data/lib/generators/stratosphere/attachment/USAGE +8 -0
  21. data/lib/generators/stratosphere/attachment/attachment_generator.rb +18 -0
  22. data/lib/generators/stratosphere/install/USAGE +0 -0
  23. data/lib/generators/stratosphere/install/install_generator.rb +26 -0
  24. data/lib/generators/stratosphere/install/templates/stratosphere.rb.erb +10 -0
  25. data/lib/stratosphere.rb +23 -0
  26. data/lib/stratosphere/attachment.rb +51 -0
  27. data/lib/stratosphere/aws.rb +50 -0
  28. data/lib/stratosphere/config.rb +5 -0
  29. data/lib/stratosphere/engine.rb +5 -0
  30. data/lib/stratosphere/has_attachment.rb +63 -0
  31. data/lib/stratosphere/image.rb +65 -0
  32. data/lib/stratosphere/style.rb +23 -0
  33. data/lib/stratosphere/version.rb +3 -0
  34. data/lib/stratosphere/video.rb +63 -0
  35. data/lib/tasks/stratosphere_tasks.rake +4 -0
  36. data/test/dummy/README.rdoc +28 -0
  37. data/test/dummy/Rakefile +6 -0
  38. data/test/dummy/app/assets/javascripts/application.js +13 -0
  39. data/test/dummy/app/controllers/application_controller.rb +5 -0
  40. data/test/dummy/app/helpers/application_helper.rb +2 -0
  41. data/test/dummy/app/views/layouts/application.html.erb +13 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/bin/setup +29 -0
  46. data/test/dummy/config.ru +4 -0
  47. data/test/dummy/config/application.rb +26 -0
  48. data/test/dummy/config/boot.rb +5 -0
  49. data/test/dummy/config/database.yml +25 -0
  50. data/test/dummy/config/environment.rb +5 -0
  51. data/test/dummy/config/environments/development.rb +41 -0
  52. data/test/dummy/config/environments/production.rb +79 -0
  53. data/test/dummy/config/environments/test.rb +42 -0
  54. data/test/dummy/config/initializers/assets.rb +11 -0
  55. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  57. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  58. data/test/dummy/config/initializers/inflections.rb +16 -0
  59. data/test/dummy/config/initializers/mime_types.rb +4 -0
  60. data/test/dummy/config/initializers/session_store.rb +3 -0
  61. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  62. data/test/dummy/config/locales/en.yml +23 -0
  63. data/test/dummy/config/routes.rb +4 -0
  64. data/test/dummy/config/secrets.yml +22 -0
  65. data/test/dummy/log/test.log +0 -0
  66. data/test/dummy/public/404.html +67 -0
  67. data/test/dummy/public/422.html +67 -0
  68. data/test/dummy/public/500.html +66 -0
  69. data/test/dummy/public/favicon.ico +0 -0
  70. data/test/integration/navigation_test.rb +10 -0
  71. data/test/stratosphere_test.rb +7 -0
  72. data/test/test_helper.rb +19 -0
  73. metadata +218 -0
@@ -0,0 +1,9 @@
1
+ /*!
2
+ * @overview RSVP - a tiny implementation of Promises/A+.
3
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
4
+ * @license Licensed under MIT license
5
+ * See https://raw.githubusercontent.com/tildeio/rsvp.js/master/LICENSE
6
+ * @version 3.0.16
7
+ */
8
+
9
+ (function(){"use strict";function t(t,n){for(var r=0,e=t.length;e>r;r++)if(t[r]===n)return r;return-1}function n(t){var n=t._promiseCallbacks;return (n||(n=t._promiseCallbacks={}), n)}function r(t,n){return"onerror"===t?void mn.on("error",n):2!==arguments.length?mn[t]:void(mn[t]=n)}function e(t){return"function"==typeof t||"object"==typeof t&&null!==t}function o(t){return"function"==typeof t}function i(t){return"object"==typeof t&&null!==t}function u(){}function s(){setTimeout(function(){for(var t,n=0;n<An.length;n++){t=An[n];var r=t.payload;r.guid=r.key+r.id,r.childGuid=r.key+r.childId,r.error&&(r.stack=r.error.stack),mn.trigger(t.name,t.payload)}An.length=0},50)}function a(t,n,r){1===An.push({name:t,payload:{key:n._guidKey,id:n._id,eventName:t,detail:n._result,childId:r&&r._id,label:n._label,timeStamp:bn(),error:mn["instrument-with-stack"]?new Error(n._label):null}})&&s()}function c(){return new TypeError("A promises callback cannot return that same promise.")}function f(){}function l(t){try{return t.then}catch(n){return (Cn.error = n, Cn)}}function h(t,n,r,e){try{t.call(n,r,e)}catch(o){return o}}function p(t,n,r){mn.async(function(t){var e=!1,o=h(r,n,function(r){e||(e=!0,n!==r?d(t,r):m(t,r))},function(n){e||(e=!0,w(t,n))},"Settle: "+(t._label||" unknown promise"));!e&&o&&(e=!0,w(t,o))},t)}function _(t,n){n._state===kn?m(t,n._result):n._state===Sn?(n._onError=null,w(t,n._result)):g(n,void 0,function(r){n!==r?d(t,r):m(t,r)},function(n){w(t,n)})}function v(t,n){if(n.constructor===t.constructor)_(t,n);else{var r=l(n);r===Cn?w(t,Cn.error):void 0===r?m(t,n):o(r)?p(t,n,r):m(t,n)}}function d(t,n){t===n?m(t,n):e(n)?v(t,n):m(t,n)}function y(t){t._onError&&t._onError(t._result),b(t)}function m(t,n){t._state===Tn&&(t._result=n,t._state=kn,0===t._subscribers.length?mn.instrument&&jn("fulfilled",t):mn.async(b,t))}function w(t,n){t._state===Tn&&(t._state=Sn,t._result=n,mn.async(y,t))}function g(t,n,r,e){var o=t._subscribers,i=o.length;t._onError=null,o[i]=n,o[i+kn]=r,o[i+Sn]=e,0===i&&t._state&&mn.async(b,t)}function b(t){var n=t._subscribers,r=t._state;if(mn.instrument&&jn(r===kn?"fulfilled":"rejected",t),0!==n.length){for(var e,o,i=t._result,u=0;u<n.length;u+=3)e=n[u],o=n[u+r],e?j(r,e,o,i):o(i);t._subscribers.length=0}}function E(){this.error=null}function A(t,n){try{return t(n)}catch(r){return (On.error = r, On)}}function j(t,n,r,e){var i,u,s,a,f=o(r);if(f){if(i=A(r,e),i===On?(a=!0,u=i.error,i=null):s=!0,n===i)return void w(n,c())}else i=e,s=!0;n._state!==Tn||(f&&s?d(n,i):a?w(n,u):t===kn?m(n,i):t===Sn&&w(n,i))}function T(t,n){var r=!1;try{n(function(n){r||(r=!0,d(t,n))},function(n){r||(r=!0,w(t,n))})}catch(e){w(t,e)}}function k(t,n,r){return t===kn?{state:"fulfilled",value:r}:{state:"rejected",reason:r}}function S(t,n,r,e){this._instanceConstructor=t,this.promise=new t(f,e),this._abortOnReject=r,this._validateInput(n)?(this._input=n,this.length=n.length,this._remaining=n.length,this._init(),0===this.length?m(this.promise,this._result):(this.length=this.length||0,this._enumerate(),0===this._remaining&&m(this.promise,this._result))):w(this.promise,this._validationError())}function C(t,n){return new In(this,t,!0,n).promise}function O(t,n){function r(t){d(i,t)}function e(t){w(i,t)}var o=this,i=new o(f,n);if(!gn(t))return (w(i,new TypeError("You must pass an array to race.")), i);for(var u=t.length,s=0;i._state===Tn&&u>s;s++)g(o.resolve(t[s]),void 0,r,e);return i}function I(t,n){var r=this;if(t&&"object"==typeof t&&t.constructor===r)return t;var e=new r(f,n);return (d(e,t), e)}function R(t,n){var r=this,e=new r(f,n);return (w(e,t), e)}function x(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function M(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function N(t,n){this._id=Yn++,this._label=n,this._state=void 0,this._result=void 0,this._subscribers=[],mn.instrument&&jn("created",this),f!==t&&(o(t)||x(),this instanceof N||M(),T(this,t))}function P(){this.value=void 0}function Y(t){try{return t.then}catch(n){return (Kn.value = n, Kn)}}function D(t,n,r){try{t.apply(n,r)}catch(e){return (Kn.value = e, Kn)}}function K(t,n){for(var r,e,o={},i=t.length,u=new Array(i),s=0;i>s;s++)u[s]=t[s];for(e=0;e<n.length;e++)r=n[e],o[r]=u[e+1];return o}function U(t){for(var n=t.length,r=new Array(n-1),e=1;n>e;e++)r[e-1]=t[e];return r}function q(t,n){return{then:function(r,e){return t.call(n,r,e)}}}function F(t,n){var r=function(){for(var r,e=this,o=arguments.length,i=new Array(o+1),u=!1,s=0;o>s;++s){if(r=arguments[s],!u){if(u=V(r),u===Un){var a=new Dn(f);return (w(a,Un.value), a)}u&&u!==!0&&(r=q(u,r))}i[s]=r}var c=new Dn(f);return (i[o]=function(t,r){t?w(c,t):void 0===n?d(c,r):n===!0?d(c,U(arguments)):gn(n)?d(c,K(arguments,n)):d(c,r)}, u?L(c,i,t,e):G(c,i,t,e))};return (r.__proto__=t, r)}function G(t,n,r,e){var o=D(r,e,n);return (o===Kn&&w(t,o.value), t)}function L(t,n,r,e){return Dn.all(n).then(function(n){var o=D(r,e,n);return (o===Kn&&w(t,o.value), t)})}function V(t){return t&&"object"==typeof t?t.constructor===Dn?!0:Y(t):!1}function W(t,n){return Dn.all(t,n)}function $(t,n,r){this._superConstructor(t,n,!1,r)}function z(t,n){return new $(Dn,t,n).promise}function B(t,n){return Dn.race(t,n)}function H(t,n,r){this._superConstructor(t,n,!0,r)}function J(t,n){return new Vn(Dn,t,n).promise}function Q(t,n,r){this._superConstructor(t,n,!1,r)}function X(t,n){return new Q(Dn,t,n).promise}function Z(t){throw (setTimeout(function(){throw t}), t)}function tn(t){var n={};return (n.promise=new Dn(function(t,r){n.resolve=t,n.reject=r},t), n)}function nn(t,n,r){return Dn.all(t,r).then(function(t){if(!o(n))throw new TypeError("You must pass a function as map's second argument.");for(var e=t.length,i=new Array(e),u=0;e>u;u++)i[u]=n(t[u]);return Dn.all(i,r)})}function rn(t,n){return Dn.resolve(t,n)}function en(t,n){return Dn.reject(t,n)}function on(t,n,r){return Dn.all(t,r).then(function(t){if(!o(n))throw new TypeError("You must pass a function as filter's second argument.");for(var e=t.length,i=new Array(e),u=0;e>u;u++)i[u]=n(t[u]);return Dn.all(i,r).then(function(n){for(var r=new Array(e),o=0,i=0;e>i;i++)n[i]&&(r[o]=t[i],o++);return (r.length=o, r)})})}function un(t,n){sr[tr]=t,sr[tr+1]=n,tr+=2,2===tr&&$n()}function sn(){var t=process.nextTick,n=process.versions.node.match(/^(?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)$/);return (Array.isArray(n)&&"0"===n[1]&&"10"===n[2]&&(t=setImmediate), function(){t(hn)})}function an(){return function(){vertxNext(hn)}}function cn(){var t=0,n=new or(hn),r=document.createTextNode("");return (n.observe(r,{characterData:!0}), function(){r.data=t=++t%2})}function fn(){var t=new MessageChannel;return (t.port1.onmessage=hn, function(){t.port2.postMessage(0)})}function ln(){return function(){setTimeout(hn,1)}}function hn(){for(var t=0;tr>t;t+=2){var n=sr[t],r=sr[t+1];n(r),sr[t]=void 0,sr[t+1]=void 0}tr=0}function pn(){try{{var t=require("vertx");t.runOnLoop||t.runOnContext}return an()}catch(n){return ln()}}function _n(t,n){mn.async(t,n)}function vn(){mn.on.apply(mn,arguments)}function dn(){mn.off.apply(mn,arguments)}var yn={mixin:function(t){return (t.on=this.on, t.off=this.off, t.trigger=this.trigger, t._promiseCallbacks=void 0, t)},on:function(r,e){var o,i=n(this);o=i[r],o||(o=i[r]=[]),-1===t(o,e)&&o.push(e)},off:function(r,e){var o,i,u=n(this);return e?(o=u[r],i=t(o,e),void(-1!==i&&o.splice(i,1))):void(u[r]=[])},trigger:function(t,r){var e,o,i=n(this);if(e=i[t])for(var u=0;u<e.length;u++)(o=e[u])(r)}},mn={instrument:!1};yn.mixin(mn);var wn;wn=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var gn=wn,bn=Date.now||function(){return(new Date).getTime()},En=Object.create||function(t){if(arguments.length>1)throw new Error("Second argument not supported");if("object"!=typeof t)throw new TypeError("Argument must be an object");return (u.prototype=t, new u)},An=[],jn=a,Tn=void 0,kn=1,Sn=2,Cn=new E,On=new E,In=S;S.prototype._validateInput=function(t){return gn(t)},S.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},S.prototype._init=function(){this._result=new Array(this.length)},S.prototype._enumerate=function(){for(var t=this.length,n=this.promise,r=this._input,e=0;n._state===Tn&&t>e;e++)this._eachEntry(r[e],e)},S.prototype._eachEntry=function(t,n){var r=this._instanceConstructor;i(t)?t.constructor===r&&t._state!==Tn?(t._onError=null,this._settledAt(t._state,n,t._result)):this._willSettleAt(r.resolve(t),n):(this._remaining--,this._result[n]=this._makeResult(kn,n,t))},S.prototype._settledAt=function(t,n,r){var e=this.promise;e._state===Tn&&(this._remaining--,this._abortOnReject&&t===Sn?w(e,r):this._result[n]=this._makeResult(t,n,r)),0===this._remaining&&m(e,this._result)},S.prototype._makeResult=function(t,n,r){return r},S.prototype._willSettleAt=function(t,n){var r=this;g(t,void 0,function(t){r._settledAt(kn,n,t)},function(t){r._settledAt(Sn,n,t)})};var Rn=C,xn=O,Mn=I,Nn=R,Pn="rsvp_"+bn()+"-",Yn=0,Dn=N;N.cast=Mn,N.all=Rn,N.race=xn,N.resolve=Mn,N.reject=Nn,N.prototype={constructor:N,_guidKey:Pn,_onError:function(t){mn.async(function(n){setTimeout(function(){n._onError&&mn.trigger("error",t)},0)},this)},then:function(t,n,r){var e=this,o=e._state;if(o===kn&&!t||o===Sn&&!n)return (mn.instrument&&jn("chained",this,this), this);e._onError=null;var i=new this.constructor(f,r),u=e._result;if(mn.instrument&&jn("chained",e,i),o){var s=arguments[o-1];mn.async(function(){j(o,i,s,u)})}else g(e,i,t,n);return i},"catch":function(t,n){return this.then(null,t,n)},"finally":function(t,n){var r=this.constructor;return this.then(function(n){return r.resolve(t()).then(function(){return n})},function(n){return r.resolve(t()).then(function(){throw n})},n)}};var Kn=new P,Un=new P,qn=F,Fn=W;$.prototype=En(In.prototype),$.prototype._superConstructor=In,$.prototype._makeResult=k,$.prototype._validationError=function(){return new Error("allSettled must be called with an array")};var Gn=z,Ln=B,Vn=H;H.prototype=En(In.prototype),H.prototype._superConstructor=In,H.prototype._init=function(){this._result={}},H.prototype._validateInput=function(t){return t&&"object"==typeof t},H.prototype._validationError=function(){return new Error("Promise.hash must be called with an object")},H.prototype._enumerate=function(){var t=this.promise,n=this._input,r=[];for(var e in n)t._state===Tn&&n.hasOwnProperty(e)&&r.push({position:e,entry:n[e]});var o=r.length;this._remaining=o;for(var i,u=0;t._state===Tn&&o>u;u++)i=r[u],this._eachEntry(i.entry,i.position)};var Wn=J;Q.prototype=En(Vn.prototype),Q.prototype._superConstructor=In,Q.prototype._makeResult=k,Q.prototype._validationError=function(){return new Error("hashSettled must be called with an object")};var $n,zn=X,Bn=Z,Hn=tn,Jn=nn,Qn=rn,Xn=en,Zn=on,tr=0,nr=un,rr="undefined"!=typeof window?window:void 0,er=rr||{},or=er.MutationObserver||er.WebKitMutationObserver,ir="undefined"!=typeof process&&"[object process]"==={}.toString.call(process),ur="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,sr=new Array(1e3);$n=ir?sn():or?cn():ur?fn():void 0===rr&&"function"==typeof require?pn():ln(),mn.async=nr;if("undefined"!=typeof window&&"object"==typeof window.__PROMISE_INSTRUMENTATION__){var ar=window.__PROMISE_INSTRUMENTATION__;r("instrument",!0);for(var cr in ar)ar.hasOwnProperty(cr)&&vn(cr,ar[cr])}var fr={race:Ln,Promise:Dn,allSettled:Gn,hash:Wn,hashSettled:zn,denodeify:qn,on:vn,off:dn,map:Jn,filter:Zn,resolve:Qn,reject:Xn,all:Fn,rethrow:Bn,defer:Hn,EventTarget:yn,configure:r,async:_n};"function"==typeof define&&define.amd?define(function(){return fr}):"undefined"!=typeof module&&module.exports?module.exports=fr:"undefined"!=typeof this&&(this.RSVP=fr)}).call(this);
@@ -0,0 +1,42 @@
1
+ <% attachment = model.send(model.class.attachment_name) %>
2
+ <div class="stratosphere-container" data-stratosphere-enabled="true" data-stratosphere-container-type="field" data-model-name="<%= model.class.to_s.underscore.pluralize %>" data-model-id="<%= model.id %>" data-attachment-name="<%= model.class.attachment_name.to_s %>">
3
+ <label class="statosphere-uploader-label">
4
+ <%= model.class.attachment_name.to_s.capitalize %>
5
+ </label>
6
+ <div class="upload-controls">
7
+ <div class="current-attachment" style="display: <%= (attachment.exists? || attachment.has_default?) ? 'inline-block' : 'none' %>; margin-right: 5px;">
8
+ <% if attachment.type == :image %>
9
+ <%= image_tag attachment.url, width: 32, height: 32, style: 'display: inline-block;' %>
10
+ <%= link_to attachment.file_name, attachment.url, target: '_blank', class: 'file-name', style: 'margin-left: 5px;' %>
11
+ <% elsif attachment.type == :video %>
12
+ <div>
13
+ <video preload="auto" width="320" height="240" controls>
14
+ <source src="<%= attachment.url %>" type="<%= attachment.mime_type %>">
15
+ </video>
16
+ </div>
17
+ <%= link_to attachment.file_name, attachment.url, target: '_blank', class: 'file-name', style: 'margin-left: 5px;' %>
18
+ <% else %>
19
+ <%= link_to attachment.file_name, attachment.url, target: '_blank', class: 'file-name' %>
20
+ <% end %>
21
+ </div>
22
+ <div class="actions" style="display: inline-block;">
23
+ <div class="upload-container" style="position: relative; display: inline-block;">
24
+ <% if attachment.type == :image %>
25
+ <input type="file" accept="image/*" style="position: absolute; opacity: 0; pointer-events: none;">
26
+ <% elsif attachment.type == :video %>
27
+ <input type="file" accept="video/*" style="position: absolute; opacity: 0; pointer-events: none;">
28
+ <% else %>
29
+ <input type="file" accept="*/*" style="position: absolute; opacity: 0; pointer-events: none;">
30
+ <% end %>
31
+ <button class="btn btn-xs btn-primary" data-stratosphere-action="upload">Upload</button>
32
+ </div>
33
+ <button class="btn btn-xs btn-danger" data-stratosphere-action="delete" style="display: <%= attachment.exists? ? 'initial' : 'none' %>; margin-left: 3px;">Delete</button>
34
+ </div>
35
+ </div>
36
+ <div class="upload-progress" style="display: none;">
37
+ <div class="progress">
38
+ <div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0;">
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
@@ -0,0 +1,56 @@
1
+ ActionController::Base.class_eval do
2
+ before_action do
3
+ if params[:stratosphere_submitted]
4
+ if stratosphere_model
5
+ case action_name
6
+ when 'edit'
7
+ @upload_params = {
8
+ file_name: params[:file_name],
9
+ content_type: params[:content_type],
10
+ content_length: params[:content_length]
11
+ }
12
+ render_stratosphere_upload_url
13
+ when 'update'
14
+ if params[:crop_params]
15
+ @crop_params = params[:crop_params]
16
+ render_attachment_crop
17
+ else
18
+ @update_params = {}
19
+ attachment_name = stratosphere_model.attachment_name
20
+ %W(#{attachment_name}_file #{attachment_name}_content_type #{attachment_name}_content_length).each do |param|
21
+ @update_params[:"#{param}"] = (param == "#{attachment_name}_content_length") ? params[:"#{param}"].to_i : params[:"#{param}"]
22
+ end
23
+ render_attachment_update
24
+ end
25
+ else
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ protected
33
+ def stratosphere_model
34
+ model = self.class.to_s.gsub!('Controller', '').singularize.safe_constantize
35
+ model && model.respond_to?(:has_attachment) ? model : nil
36
+ end
37
+
38
+ def stratosphere_upload_url
39
+ stratosphere_model.find(params[:id]).send(:"#{stratosphere_model.attachment_name}").presigned_upload(@upload_params)
40
+ end
41
+
42
+ def render_stratosphere_upload_url
43
+ render json: { url: stratosphere_upload_url }
44
+ end
45
+
46
+ def render_attachment_update
47
+ render json: stratosphere_model.find(params[:id]).update!(@update_params)
48
+ end
49
+
50
+ def render_attachment_crop
51
+ model = stratosphere_model.find(params[:id])
52
+ attachment = model.send(:"#{stratosphere_model.attachment_name}")
53
+ attachment.crop(@crop_params)
54
+ render json: attachment
55
+ end
56
+ end
@@ -0,0 +1,2 @@
1
+ Stratosphere::Engine.routes.draw do
2
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a migration to support a stratosphere attachment on your model. The NAME argument represents the model you would like to add an attachment to and the ATTACHMENT_NAME represents the name of the attachment. i.e. rails generate attachment user avatar.
3
+
4
+ Example:
5
+ rails generate attachment model attachment
6
+
7
+ This will create:
8
+ db/migrate/add_attachment_to_model.rb
@@ -0,0 +1,18 @@
1
+ module Stratosphere
2
+ module Generators
3
+ class AttachmentGenerator < ::Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ desc "Generate a migration to support a stratosphere attachment on your model. The #{"\033[32mNAME\033[0m"} " <<
7
+ "argument represents the model you would like to add an attachment to and the #{"\033[36mATTACHMENT_NAME\033[0m"} " <<
8
+ "represents the name of the attachment. i.e. rails generate attachment #{"\033[32muser\033[0m"} #{"\033[36mavatar\033[0m"}."
9
+
10
+ argument :model_name, required: true, type: :string
11
+ argument :attachment_name, required: true, type: :string, desc: 'A name for the attachment you would like to add.'
12
+
13
+ def create_migration
14
+ generate 'migration', "AddAttachmentTo#{model_name.classify.pluralize} #{attachment_name}_file:string #{attachment_name}_content_type:string #{attachment_name}_content_length:int8"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module Stratosphere
2
+ module Generators
3
+ class InstallGenerator < ::Rails::Generators::Base
4
+ attr_accessor :key, :secret, :bucket
5
+
6
+ source_root File.expand_path('../../../../../', __FILE__)
7
+
8
+ desc 'Add stratosphere views for easy attachment uploads via AJAX.'
9
+
10
+ def set_options
11
+ STDOUT.flush
12
+ print 'What is your S3 bucket name?: '
13
+ @bucket = gets.chomp
14
+ end
15
+
16
+ def create_initializer
17
+ template 'lib/generators/stratosphere/install/templates/stratosphere.rb.erb', 'config/initializers/stratosphere.rb'
18
+ end
19
+
20
+ def create_views
21
+ copy_file 'app/views/_attachment_field.html.erb', 'app/views/stratosphere/_attachment_field.html.erb'
22
+ copy_file 'app/assets/javascripts/stratosphere/stratosphere.bundled.min.js', 'vendor/assets/javascripts/stratosphere.bundled.min.js'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ Stratosphere.configure do |config|
2
+ config.cloud = :aws
3
+ config.domain = 'http://s3.amazonaws.com/<%= @bucket %>'
4
+ config.aws = {
5
+ access_key: ENV['AWS_ACCESS_KEY_ID'],
6
+ secret: ENV['AWS_SECRET_ACCESS_KEY'],
7
+ region: ENV['AWS_REGION'],
8
+ s3_bucket: '<%= @bucket %>'
9
+ }
10
+ end
@@ -0,0 +1,23 @@
1
+ require 'aws-sdk'
2
+ require 'stratosphere/aws'
3
+ require 'stratosphere/attachment'
4
+ require 'stratosphere/config'
5
+ require 'stratosphere/engine'
6
+ require 'stratosphere/has_attachment'
7
+ require 'stratosphere/image'
8
+ require 'stratosphere/style'
9
+ require 'stratosphere/video'
10
+
11
+ module Stratosphere
12
+ class << self
13
+ attr_writer :config
14
+ end
15
+
16
+ def self.config
17
+ @configuration ||= Stratosphere::Config.new
18
+ end
19
+
20
+ def self.configure
21
+ yield config
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ module Stratosphere
2
+ class Attachment
3
+ attr_accessor :base_path, :config, :file_name, :name, :size, :mime_type, :type, :owner, :file_store
4
+
5
+ def initialize(owner, name, options={})
6
+ @config = Stratosphere.config
7
+ @name = name
8
+ @owner = owner
9
+ @file_name = @owner["#{@name}_file"]
10
+ @file_size = @owner["#{@name}_content_length"]
11
+ @mime_type = @owner["#{@name}_content_type"]
12
+ @type = :attachment
13
+ @file_store = Stratosphere::AWS::S3.new
14
+ set_base_path
15
+ end
16
+
17
+ def set_base_path
18
+ plural_attr = name.to_s.pluralize
19
+ plural_model = owner.class.to_s.downcase.pluralize
20
+
21
+ if config.dir_prefix
22
+ prefix = config.dir_prefix[-1, 1] == '/' ? config.dir_prefix.slice(0, -1) : config.dir_prefix
23
+ @base_path = "#{prefix}/#{plural_model}/#{plural_attr}/#{owner.id}"
24
+ else
25
+ @base_path = "#{plural_model}/#{plural_attr}/#{owner.id}"
26
+ end
27
+ end
28
+
29
+ def exists?
30
+ !file_name.nil?
31
+ end
32
+
33
+ def has_default?
34
+ false
35
+ end
36
+
37
+ def url
38
+ "#{config.domain}/#{base_path}/#{file_name}" if file_name
39
+ end
40
+
41
+ def presigned_upload(options)
42
+ options.merge!(key: "#{base_path}/#{options[:file_name]}")
43
+ options.delete :file_name
44
+ file_store.presigned_upload options
45
+ end
46
+
47
+ def destroy!
48
+ file_store.delete_objects base_path
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ module Stratosphere
2
+ module AWS
3
+ class S3
4
+ attr_accessor :credentials, :resource, :region, :bucket_name, :presigner
5
+
6
+ def initialize
7
+ key = Stratosphere.config.aws[:access_key]
8
+ secret = Stratosphere.config.aws[:secret]
9
+ @region = Stratosphere.config.aws[:region]
10
+ @bucket_name = Stratosphere.config.aws[:s3_bucket]
11
+ @credentials = Aws::Credentials.new(key, secret)
12
+ @resource = Aws::S3::Resource.new(credentials: @credentials, region: @region)
13
+ @presigner = Aws::S3::Presigner.new(region: @region)
14
+ Aws.config[:region] = @region
15
+ Aws.config[:credentials] = @credentials
16
+ end
17
+
18
+ def bucket
19
+ resource.bucket bucket_name
20
+ end
21
+
22
+ def delete_objects(prefix)
23
+ threads = []
24
+ bucket.objects(prefix: prefix).limit(50).each { |object| threads.push Thread.new { object.delete if object } }
25
+ threads.each(&:join)
26
+ end
27
+
28
+ def upload(options={})
29
+ bucket.put_object(options)
30
+ end
31
+
32
+ def presigned_upload(options={})
33
+ params = options.keep_if { |k,v| [:key, :content_type, :content_length].include? k }.merge!(bucket: bucket_name)
34
+ presigner.presigned_url(:put_object, params)
35
+ end
36
+ end
37
+
38
+ class ElasticTranscoder
39
+ def self.encoder
40
+ key = Stratosphere.config.aws[:access_key]
41
+ secret = Stratosphere.config.aws[:secret]
42
+ Aws::ElasticTranscoder::Client.new(credentials: Aws::Credentials.new(key, secret), region: Stratosphere.config.aws[:region])
43
+ end
44
+
45
+ def self.create_job(params)
46
+ encoder.create_job(params)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ module Stratosphere
2
+ class Config
3
+ attr_accessor :aws, :cloud, :domain, :dir_prefix
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Stratosphere
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Stratosphere
4
+ end
5
+ end
@@ -0,0 +1,63 @@
1
+ module Stratosphere
2
+ module HasAttachment
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def has_attachment(name, options={})
7
+ cattr_accessor :attachment_name, :attachment_type
8
+
9
+ self.attachment_name = name.to_sym
10
+ self.attachment_type = options[:type]
11
+
12
+ define_method "#{name}" do
13
+ case options[:type]
14
+ when :image
15
+ Stratosphere::Image.new(self, name, options)
16
+ when :video
17
+ Stratosphere::Video.new(self, name, options)
18
+ else
19
+ Stratosphere::Attachment.new(self, name, options)
20
+ end
21
+ end
22
+
23
+ send(:before_save) do
24
+ if send(:"#{name}_file_changed?")
25
+ attr = self[:"#{name}_file"]
26
+
27
+ if [:image, :video].include? options[:type]
28
+ if options[:type] == :image
29
+ @attachment = Stratosphere::Image.new(self, name, options)
30
+ elsif options[:type] == :video
31
+ @attachment = Stratosphere::Video.new(self, name, options)
32
+ end
33
+ else
34
+ @attachment = Stratosphere::Attachment.new(self, name, options)
35
+ end
36
+
37
+ if attr.nil? || attr.chomp == ''
38
+ self[:"#{name}_file"] = nil
39
+ self[:"#{name}_content_type"] = nil
40
+ self[:"#{name}_content_length"] = nil
41
+ @attachment.destroy!
42
+ else
43
+ @attachment.encode if @attachment.type == :video
44
+ end
45
+ end
46
+ end
47
+
48
+ self.send(:before_destroy) do
49
+ case options[:type]
50
+ when :image
51
+ Stratosphere::Image.new(self, name, options).destroy!
52
+ when :video
53
+ Stratosphere::Video.new(self, name, options).destroy!
54
+ else
55
+ Stratosphere::Attachment.new(self, name, options).destroy!
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ ActiveRecord::Base.send :include, Stratosphere::HasAttachment
@@ -0,0 +1,65 @@
1
+ require 'open-uri'
2
+ require 'rmagick'
3
+
4
+ module Stratosphere
5
+ class Image < Attachment
6
+ attr_accessor :default, :styles, :crop_params
7
+
8
+ def initialize(owner, name, options={})
9
+ super
10
+ @type = :image
11
+ @styles = []
12
+ @default = options[:default]
13
+ if @file_name && options[:styles] && options[:styles].count > 0
14
+ options[:styles].each { |style| @styles.push Stratosphere::Style.new( style.merge(file_name: @file_name) ) }
15
+ end
16
+ end
17
+
18
+ def has_default?
19
+ !default.nil?
20
+ end
21
+
22
+ def url(style_name=:original)
23
+ url = default ? "#{config.domain}/#{default}" : nil
24
+ if file_name
25
+ url = "#{config.domain}/#{base_path}/#{style_name.to_s}/#{file_name}"
26
+ unless style_name == :original
27
+ style = styles.count > 0 ? self.styles.select { |style| style.name == style_name }.first : nil
28
+ url.gsub!(file_name, style.file_name) unless style.nil?
29
+ end
30
+ end
31
+ url
32
+ end
33
+
34
+ def presigned_upload(options)
35
+ options.merge!(key: "#{base_path}/original/#{options[:file_name]}")
36
+ options.delete :file_name
37
+ file_store.presigned_upload options
38
+ end
39
+
40
+ def crop(x, y, w, h)
41
+ if styles.count > 0
42
+ begin
43
+ io = open(url)
44
+ file = Magick::Image.from_blob(io.read).first.crop(x.to_i, y.to_i, w.to_i, h.to_i)
45
+ threads = []
46
+ io.close
47
+ styles.each do |style|
48
+ if style.dimensions
49
+ t = Thread.new do
50
+ k = "#{base_path}/#{style.name}/#{file_name}"
51
+ r = file.resize(style.dimensions[0], style.dimensions[1])
52
+ Stratosphere.file_store.upload(key: k, content_type: 'image/jpeg', body: r.to_blob)
53
+ end
54
+ threads.push(t)
55
+ end
56
+ end
57
+ threads.each(&:join)
58
+ rescue OpenURI::HTTPError => e
59
+ puts "Error: Original image not found at '#{url}'"
60
+ puts e
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end