stratosphere 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|