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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +262 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/stratosphere/inflection.min.js +22 -0
- data/app/assets/javascripts/stratosphere/rsvp.min.js +9 -0
- data/app/assets/javascripts/stratosphere/stratosphere.bundled.min.js +31 -0
- data/app/assets/javascripts/stratosphere/stratosphere.js +297 -0
- data/app/controllers/stratosphere/application_controller.rb +4 -0
- data/app/helpers/stratosphere/application_helper.rb +4 -0
- data/app/vendor/assets/javascripts/rsvp/README.md +9 -0
- data/app/vendor/assets/javascripts/rsvp/bower.json +22 -0
- data/app/vendor/assets/javascripts/rsvp/composer.json +18 -0
- data/app/vendor/assets/javascripts/rsvp/package.json +59 -0
- data/app/vendor/assets/javascripts/rsvp/rsvp.js +1671 -0
- data/app/vendor/assets/javascripts/rsvp/rsvp.min.js +9 -0
- data/app/views/_attachment_field.html.erb +42 -0
- data/config/initializers/action_controller.rb +56 -0
- data/config/routes.rb +2 -0
- data/lib/generators/stratosphere/attachment/USAGE +8 -0
- data/lib/generators/stratosphere/attachment/attachment_generator.rb +18 -0
- data/lib/generators/stratosphere/install/USAGE +0 -0
- data/lib/generators/stratosphere/install/install_generator.rb +26 -0
- data/lib/generators/stratosphere/install/templates/stratosphere.rb.erb +10 -0
- data/lib/stratosphere.rb +23 -0
- data/lib/stratosphere/attachment.rb +51 -0
- data/lib/stratosphere/aws.rb +50 -0
- data/lib/stratosphere/config.rb +5 -0
- data/lib/stratosphere/engine.rb +5 -0
- data/lib/stratosphere/has_attachment.rb +63 -0
- data/lib/stratosphere/image.rb +65 -0
- data/lib/stratosphere/style.rb +23 -0
- data/lib/stratosphere/version.rb +3 -0
- data/lib/stratosphere/video.rb +63 -0
- data/lib/tasks/stratosphere_tasks.rake +4 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +13 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/log/test.log +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/stratosphere_test.rb +7 -0
- data/test/test_helper.rb +19 -0
- 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
|
data/config/routes.rb
ADDED
@@ -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
|
File without changes
|
@@ -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
|
data/lib/stratosphere.rb
ADDED
@@ -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,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
|