larsklevan-attachment_fu_app_engine 0.2.0
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.
- data/MIT-LICENSE +20 -0
- data/README +62 -0
- data/Rakefile +22 -0
- data/app_engine/attachment_fu_app_engine/app.yaml +8 -0
- data/app_engine/attachment_fu_app_engine/attachment.py +219 -0
- data/app_engine/attachment_fu_app_engine/index.yaml +17 -0
- data/app_engine/attachment_fu_app_engine/new.html +17 -0
- data/app_engine/attachment_fu_app_engine/photo.pyc +0 -0
- data/attachment_fu_app_engine.gemspec +13 -0
- data/init.rb +0 -0
- data/initializer.rb.tpl +6 -0
- data/install.rb +10 -0
- data/lib/multipart_post.rb +30 -0
- data/lib/tasks/migrate.rake +37 -0
- data/lib/technoweenie/attachment_fu/backends/app_engine_backend.rb +72 -0
- metadata +67 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Lars Klevan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
AttachmentFuAppEngine
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing. Removes the need to install ImageMagick and allows you to add or change thumbnail sizes without migrating previous data.
|
5
|
+
|
6
|
+
The Google App Engine backend code is included so you can run it in your own App Engine instance.
|
7
|
+
|
8
|
+
Example
|
9
|
+
=======
|
10
|
+
|
11
|
+
class Photo < ActiveRecord::Base
|
12
|
+
has_attachment :storage => :app_engine, :thumbnails => {:small_square => "45x45!"}
|
13
|
+
end
|
14
|
+
|
15
|
+
AttachmentFuAppEngine attempts to reproduce the resize format from ImageMagick.
|
16
|
+
|
17
|
+
* "100x100" - scale the image to fit within a 100x100 box (the larger dimension will be 100, such as 100x75), preserving the aspect ratio. Will scale up small images.
|
18
|
+
* "100x100!" - scale the image to be exactly 100x100 by scaling proportionately and cropping off the edges. Will not distort the image but may hide part of it.
|
19
|
+
* "100x100>" - scale the image to be at most 100x100 - smaller images will not be enlarged.
|
20
|
+
|
21
|
+
In addition to the standard thumbnail specification:
|
22
|
+
Image.find(1).public_filename(:small_square)
|
23
|
+
|
24
|
+
you can also use:
|
25
|
+
Image.find(1).public_filename('40x40!')
|
26
|
+
|
27
|
+
|
28
|
+
Installation
|
29
|
+
============
|
30
|
+
|
31
|
+
gem install larsklevan-attachment_fu_app_engine -s http://gems.github.com
|
32
|
+
OR
|
33
|
+
script/plugin install git://github.com/larsklevan/attachment_fu_app_engine.git
|
34
|
+
|
35
|
+
|
36
|
+
Migration
|
37
|
+
=========
|
38
|
+
|
39
|
+
You can use the provided rake task to move data from S3 to the App Engine. Just run:
|
40
|
+
rake ATTACHMENT_CLASS=Image migrate_s3_to_app_engine
|
41
|
+
|
42
|
+
|
43
|
+
Configuration
|
44
|
+
=============
|
45
|
+
|
46
|
+
You can use your own instance of the App Engine code to store images so you have your own quotas. Use
|
47
|
+
the provided app engine code and configure the URL as follows:
|
48
|
+
|
49
|
+
Technoweenie::AttachmentFu::Backends::AppEngineBackend.base_url = "http://attachment-fu-gae.appspot.com"
|
50
|
+
|
51
|
+
|
52
|
+
Limitations
|
53
|
+
===========
|
54
|
+
|
55
|
+
* Tested with Rails 2.1 only
|
56
|
+
* Google App Engine is in early release, so if you go past your daily storage/bandwidth/CPU limit you are out of luck.
|
57
|
+
* Google App Engine does not support HTTPS.
|
58
|
+
* Google App Engine currently has a limit of 1MB per reocrd
|
59
|
+
|
60
|
+
For more information about the status of the App Engine see http://googleappengine.blogspot.com/2008/04/were-up-and-running.html
|
61
|
+
|
62
|
+
Copyright (c) 2008 Lars Klevan, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the attachment_fu_app_engine plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the attachment_fu_app_engine plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'AttachmentFuAppEngine'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
import os
|
2
|
+
import cgi
|
3
|
+
import logging
|
4
|
+
from StringIO import StringIO
|
5
|
+
import struct
|
6
|
+
|
7
|
+
import wsgiref.handlers
|
8
|
+
from datetime import date
|
9
|
+
|
10
|
+
from google.appengine.api import images
|
11
|
+
from google.appengine.api import memcache
|
12
|
+
from google.appengine.ext import db
|
13
|
+
from google.appengine.ext import webapp
|
14
|
+
from google.appengine.ext.webapp import template
|
15
|
+
|
16
|
+
class Attachment(db.Model):
|
17
|
+
path = db.StringProperty()
|
18
|
+
|
19
|
+
filename = db.StringProperty()
|
20
|
+
uploaded_data = db.BlobProperty()
|
21
|
+
content_type = db.StringProperty()
|
22
|
+
height = db.IntegerProperty()
|
23
|
+
width = db.IntegerProperty()
|
24
|
+
size = db.IntegerProperty()
|
25
|
+
|
26
|
+
# FROM http://www.google.com/codesearch?hl=en&q=+getImageInfo+show:RjgT7H1iBVM:V39CptbrGJ8:XcXNaKeZR3k&sa=N&cd=2&ct=rc&cs_p=http://www.zope.org/Products/Zope3/3.0.0final/ZopeX3-3.0.0.tgz&cs_f=ZopeX3-3.0.0/Dependencies/zope.app.file-ZopeX3-3.0.0/zope.app.file/image.py#l88
|
27
|
+
def extract_image_attributes(self,data):
|
28
|
+
data = str(data)
|
29
|
+
size = len(data)
|
30
|
+
height = -1
|
31
|
+
width = -1
|
32
|
+
content_type = ''
|
33
|
+
|
34
|
+
# handle GIFs
|
35
|
+
if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
|
36
|
+
# Check to see if content_type is correct
|
37
|
+
content_type = 'image/gif'
|
38
|
+
w, h = struct.unpack("<HH", data[6:10])
|
39
|
+
width = int(w)
|
40
|
+
height = int(h)
|
41
|
+
|
42
|
+
# See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
|
43
|
+
# Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
|
44
|
+
# and finally the 4-byte width, height
|
45
|
+
elif ((size >= 24) and data.startswith('\211PNG\r\n\032\n') and (data[12:16] == 'IHDR')):
|
46
|
+
content_type = 'image/png'
|
47
|
+
w, h = struct.unpack(">LL", data[16:24])
|
48
|
+
width = int(w)
|
49
|
+
height = int(h)
|
50
|
+
|
51
|
+
# Maybe this is for an older PNG version.
|
52
|
+
elif (size >= 16) and data.startswith('\211PNG\r\n\032\n'):
|
53
|
+
# Check to see if we have the right content type
|
54
|
+
content_type = 'image/png'
|
55
|
+
w, h = struct.unpack(">LL", data[8:16])
|
56
|
+
width = int(w)
|
57
|
+
height = int(h)
|
58
|
+
|
59
|
+
# handle JPEGs
|
60
|
+
elif (size >= 2) and data.startswith('\377\330'):
|
61
|
+
content_type = 'image/jpeg'
|
62
|
+
jpeg = StringIO(data)
|
63
|
+
jpeg.read(2)
|
64
|
+
b = jpeg.read(1)
|
65
|
+
try:
|
66
|
+
while (b and ord(b) != 0xDA):
|
67
|
+
while (ord(b) != 0xFF): b = jpeg.read(1)
|
68
|
+
while (ord(b) == 0xFF): b = jpeg.read(1)
|
69
|
+
if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
|
70
|
+
jpeg.read(3)
|
71
|
+
h, w = struct.unpack(">HH", jpeg.read(4))
|
72
|
+
break
|
73
|
+
else:
|
74
|
+
jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2)
|
75
|
+
b = jpeg.read(1)
|
76
|
+
width = int(w)
|
77
|
+
height = int(h)
|
78
|
+
except struct.error:
|
79
|
+
pass
|
80
|
+
except ValueError:
|
81
|
+
pass
|
82
|
+
|
83
|
+
return height,width
|
84
|
+
|
85
|
+
def update_uploaded_data(self, data, content_type):
|
86
|
+
if content_type.startswith('image'):
|
87
|
+
self.height, self.width = self.extract_image_attributes(data)
|
88
|
+
if not self.height:
|
89
|
+
#if we can't determine the image attributes in the original format try converting it to a PNG with a no-op rotate
|
90
|
+
image = images.Image(data)
|
91
|
+
image.rotate(0)
|
92
|
+
self.height, self.width = self.extract_image_attributes(image.execute_transforms(output_encoding=images.PNG))
|
93
|
+
|
94
|
+
self.content_type = content_type
|
95
|
+
self.uploaded_data = data
|
96
|
+
self.size = len(data)
|
97
|
+
|
98
|
+
# I'm attempting to replicate the resize format from http://www.imagemagick.org/Usage/resize/#resize
|
99
|
+
# at least enough to be usable for avatar or photo gallery thumbnails
|
100
|
+
def resize(self, format):
|
101
|
+
preserve_aspect_ratio = True
|
102
|
+
allow_scale_up = True
|
103
|
+
if format.endswith("!"):
|
104
|
+
preserve_aspect_ratio = False
|
105
|
+
format = format.rstrip("!")
|
106
|
+
elif format.endswith(">"):
|
107
|
+
allow_scale_up = False
|
108
|
+
format = format.rstrip(">")
|
109
|
+
|
110
|
+
width,height = format.split('x')
|
111
|
+
|
112
|
+
img = images.Image(self.uploaded_data)
|
113
|
+
if not preserve_aspect_ratio:
|
114
|
+
requested_aspect = float(height)/float(width)
|
115
|
+
aspect = float(self.height)/float(self.width)
|
116
|
+
|
117
|
+
ratio = requested_aspect / aspect
|
118
|
+
if (ratio < 1):
|
119
|
+
left_x = 0.0
|
120
|
+
right_x = 1.0
|
121
|
+
top_y = 0.5 - (ratio / 2)
|
122
|
+
bottom_y = 0.5 + (ratio / 2)
|
123
|
+
else:
|
124
|
+
top_y = 0.0
|
125
|
+
bottom_y = 1.0
|
126
|
+
left_x = 0.5 - ((1/ratio) / 2)
|
127
|
+
right_x = 0.5 + ((1/ratio) / 2)
|
128
|
+
|
129
|
+
# seem to have issues with small rounding errors for larger images - request for 2000x2000 can end up at 1998x2000
|
130
|
+
# presumably rounding errors - the 0-1 scale for cropping is weird...
|
131
|
+
img.crop(left_x=left_x,top_y=top_y,right_x=right_x,bottom_y=bottom_y)
|
132
|
+
|
133
|
+
if allow_scale_up or int(width) < self.width or int(height) < self.height:
|
134
|
+
img.resize(width=int(width), height=int(height))
|
135
|
+
|
136
|
+
output_encoding, content_type = images.PNG, 'image/png'
|
137
|
+
if self.content_type == 'image/jpeg' or self.content_type == 'image/jpg':
|
138
|
+
output_encoding, content_type = images.JPEG, 'image/jpeg'
|
139
|
+
|
140
|
+
img.rotate(0) #no-op so that we don't break if we haven't done any transforms
|
141
|
+
return img.execute_transforms(output_encoding), content_type
|
142
|
+
|
143
|
+
class UploadAttachmentPage(webapp.RequestHandler):
|
144
|
+
def get(self):
|
145
|
+
path = os.path.join(os.path.dirname(__file__), 'new.html')
|
146
|
+
self.response.out.write(template.render(path, {}))
|
147
|
+
|
148
|
+
class AttachmentPage(webapp.RequestHandler):
|
149
|
+
def get(self):
|
150
|
+
attachment = None
|
151
|
+
try:
|
152
|
+
id = self.request.path.split('/')[-1]
|
153
|
+
attachment = Attachment.get(db.Key(id))
|
154
|
+
except:
|
155
|
+
None
|
156
|
+
|
157
|
+
if not attachment:
|
158
|
+
attachment = db.Query(Attachment).filter("path =", self.request.path[1::]).get()
|
159
|
+
|
160
|
+
if not attachment:
|
161
|
+
# Either "id" wasn't provided, or there was no attachment with that ID
|
162
|
+
# in the datastore.
|
163
|
+
self.error(404)
|
164
|
+
return
|
165
|
+
|
166
|
+
today = date.today()
|
167
|
+
self.response.headers.add_header("Expires", date(year=today.year + 1,month=today.month, day=today.day).ctime())
|
168
|
+
format = self.request.get("resize")
|
169
|
+
if format:
|
170
|
+
memcache_client = memcache.Client()
|
171
|
+
cache_key = "attachment-" + str(attachment.key()) + "-" + format
|
172
|
+
result = memcache_client.get(cache_key)
|
173
|
+
if not result:
|
174
|
+
data, content_type = attachment.resize(format)
|
175
|
+
memcache_client.set(cache_key, [data, content_type])
|
176
|
+
else:
|
177
|
+
data, content_type = result[0], result[1]
|
178
|
+
self.response.headers['Content-Type'] = content_type
|
179
|
+
self.response.out.write(data)
|
180
|
+
else:
|
181
|
+
self.response.headers['Content-Type'] = str(attachment.content_type)
|
182
|
+
self.response.out.write(attachment.uploaded_data)
|
183
|
+
|
184
|
+
def post(self):
|
185
|
+
form = cgi.FieldStorage()
|
186
|
+
|
187
|
+
path = form.getvalue('path')
|
188
|
+
attachment = db.Query(Attachment).filter("path =", path).get()
|
189
|
+
if not attachment:
|
190
|
+
attachment = Attachment()
|
191
|
+
attachment.path = path
|
192
|
+
|
193
|
+
uploaded_data = form['uploaded_data']
|
194
|
+
attachment.filename = uploaded_data.filename
|
195
|
+
|
196
|
+
attachment.update_uploaded_data(uploaded_data.value, uploaded_data.type)
|
197
|
+
attachment.put()
|
198
|
+
|
199
|
+
|
200
|
+
logging.debug('Added attachment with path: ' + attachment.path + ' id: ' + str(attachment.key()))
|
201
|
+
self.redirect('/attachments/' + str(attachment.key()))
|
202
|
+
|
203
|
+
|
204
|
+
class RedirectPage(webapp.RequestHandler):
|
205
|
+
def get(self):
|
206
|
+
self.redirect('/attachments/new')
|
207
|
+
|
208
|
+
def main():
|
209
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
210
|
+
|
211
|
+
application = webapp.WSGIApplication(
|
212
|
+
[('/attachments/new', UploadAttachmentPage),
|
213
|
+
('/attachments.*', AttachmentPage),
|
214
|
+
('/.*', RedirectPage)],
|
215
|
+
debug=True)
|
216
|
+
wsgiref.handlers.CGIHandler().run(application)
|
217
|
+
|
218
|
+
if __name__ == "__main__":
|
219
|
+
main()
|
@@ -0,0 +1,17 @@
|
|
1
|
+
indexes:
|
2
|
+
|
3
|
+
# AUTOGENERATED
|
4
|
+
|
5
|
+
# This index.yaml is automatically updated whenever the dev_appserver
|
6
|
+
# detects that a new type of query is run. If you want to manage the
|
7
|
+
# index.yaml file manually, remove the above marker line (the line
|
8
|
+
# saying "# AUTOGENERATED"). If you want to manage some indexes
|
9
|
+
# manually, move them above the marker line. The index.yaml file is
|
10
|
+
# automatically uploaded to the admin console when you next deploy
|
11
|
+
# your application using appcfg.py.
|
12
|
+
|
13
|
+
# Unused in query history -- copied from input.
|
14
|
+
- kind: Attachment
|
15
|
+
properties:
|
16
|
+
- name: path
|
17
|
+
direction: desc
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<html>
|
2
|
+
<body>
|
3
|
+
Upload a file:
|
4
|
+
<br/>
|
5
|
+
<br/>
|
6
|
+
<form action="/attachments" method="post" enctype="multipart/form-data">
|
7
|
+
<label for="file">Select a file:</label>
|
8
|
+
<br/>
|
9
|
+
<input type="file" name="uploaded_data" />
|
10
|
+
<br/>
|
11
|
+
<label for="file">Path:</label>
|
12
|
+
<input type="text" name="path" />
|
13
|
+
<br/>
|
14
|
+
<input type="submit" value="Submit" />
|
15
|
+
</form>
|
16
|
+
</body>
|
17
|
+
</html>
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "attachment_fu_app_engine"
|
3
|
+
s.version = "0.2.0"
|
4
|
+
s.date = "2008-08-02"
|
5
|
+
s.summary = "Extension for AttachmentFu which uses the Google App Engine for storage"
|
6
|
+
s.email = "tastybyte@gmail.com"
|
7
|
+
s.homepage = "http://github.com/larsklevan/attachment_fu_app_engine"
|
8
|
+
s.description = "Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing."
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["Lars Klevan"]
|
11
|
+
s.files = %w{app_engine/attachment_fu_app_engine/app.yaml app_engine/attachment_fu_app_engine/attachment.py app_engine/attachment_fu_app_engine/index.yaml app_engine/attachment_fu_app_engine/new.html app_engine/attachment_fu_app_engine/photo.pyc attachment_fu_app_engine.gemspec init.rb initializer.rb.tpl install.rb lib/multipart_post.rb lib/tasks/migrate.rake lib/technoweenie/attachment_fu/backends/app_engine_backend.rb MIT-LICENSE Rakefile README}
|
12
|
+
end
|
13
|
+
|
data/init.rb
ADDED
File without changes
|
data/initializer.rb.tpl
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# if you're using this in production I'd recommend creating your own app engine app with the provided backend code
|
2
|
+
# see http://code.google.com/appengine/
|
3
|
+
Technoweenie::AttachmentFu::Backends::AppEngineBackend.base_url = "http://attachment-fu-gae.appspot.com"
|
4
|
+
|
5
|
+
# storage prefix prevents collision between multiple apps using the same app engine for storage
|
6
|
+
Technoweenie::AttachmentFu::Backends::AppEngineBackend.storage_prefix = "<%= rand(10e12) %>"
|
data/install.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
initializer = File.dirname(__FILE__) + '/../../../config/initializers/app_engine_backend.rb'
|
4
|
+
|
5
|
+
unless File.exist? initializer
|
6
|
+
initializer_template = IO.read(File.dirname(__FILE__) + '/initializer.rb.tpl')
|
7
|
+
File.open(initializer, 'w') { |f| f << ERB.new(initializer_template).result }
|
8
|
+
end
|
9
|
+
|
10
|
+
puts IO.read(File.join(File.dirname(__FILE__), 'README'))
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class MultipartPost
|
2
|
+
# see http://www.realityforge.org/articles/2006/03/02/upload-a-file-via-post-with-net-http for file upload with http
|
3
|
+
def self.post(uri, params=[])
|
4
|
+
chunks = []
|
5
|
+
params.each do |param|
|
6
|
+
param[:name]
|
7
|
+
chunks << if param[:mime_type]
|
8
|
+
"Content-Disposition: form-data; name=\"#{CGI::escape(param[:name])}\"; filename=\"#{param[:filename]}\"\r\n" +
|
9
|
+
"Content-Transfer-Encoding: binary\r\n" +
|
10
|
+
"Content-Type: #{param[:mime_type]}\r\n" +
|
11
|
+
"\r\n#{param[:value]}\r\n"
|
12
|
+
else
|
13
|
+
"Content-Disposition: form-data; name=\"#{CGI::escape(param[:name])}\"\r\n" +
|
14
|
+
"\r\n#{param[:value]}\r\n"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
boundary = "349832898984244898448024464570528145"
|
18
|
+
post_body = ""
|
19
|
+
chunks.each do |chunk|
|
20
|
+
post_body << "--#{boundary}\r\n"
|
21
|
+
post_body << chunk
|
22
|
+
end
|
23
|
+
post_body << "--#{boundary}--\r\n"
|
24
|
+
|
25
|
+
uri = URI.parse(uri)
|
26
|
+
Net::HTTP.new(uri.host, uri.port).start do |http|
|
27
|
+
http.request_post(uri.path, post_body, "Content-type" => "multipart/form-data; boundary=" + boundary)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# move from s3 to gae
|
2
|
+
|
3
|
+
desc 'Migrate file data from s3 storage to app engine'
|
4
|
+
task :migrate_s3_to_app_engine => :environment do
|
5
|
+
require 'open-uri'
|
6
|
+
require 'multipart_post'
|
7
|
+
|
8
|
+
raise 'You should specify the class to migrate with ATTACHMENT_CLASS=Image'
|
9
|
+
clazz = ENV['ATTACHMENT_CLASS'].constantize
|
10
|
+
|
11
|
+
raise 'You should set the :storage option from your class to :s3 when you run this migration' unless clazz.attachment_options[:storage].to_s == 's3'
|
12
|
+
|
13
|
+
app_engine_config = Technoweenie::AttachmentFu::Backends::AppEngineBackend
|
14
|
+
|
15
|
+
failures = []
|
16
|
+
clazz.find(:all).each do |o|
|
17
|
+
next if o.respond_to?(:parent) && o.parent
|
18
|
+
begin
|
19
|
+
puts "Migrating #{o.public_filename}"
|
20
|
+
|
21
|
+
temp_data = open(o.public_filename).read
|
22
|
+
|
23
|
+
app_engine_path = ['attachments', app_engine_config.storage_prefix, clazz.attachment_options[:path_prefix], o.id, o.filename].compact.join('/')
|
24
|
+
|
25
|
+
response = MultipartPost.post(app_engine_config.base_url + '/attachments', [
|
26
|
+
{:name => 'uploaded_data', :filename => o.filename, :mime_type => o.content_type, :value => temp_data},
|
27
|
+
{:name => 'path', :value => app_engine_path}
|
28
|
+
])
|
29
|
+
raise response.body unless response.is_a? Net::HTTPRedirection
|
30
|
+
rescue => err
|
31
|
+
puts "Failed to migrate #{o.public_filename} - #{err.message}"
|
32
|
+
failures << o.id
|
33
|
+
next
|
34
|
+
end
|
35
|
+
end
|
36
|
+
puts "#{failures.size} files failed to migrate" unless failures.empty?
|
37
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'multipart_post'
|
3
|
+
|
4
|
+
module Technoweenie # :nodoc:
|
5
|
+
module AttachmentFu # :nodoc:
|
6
|
+
module Backends
|
7
|
+
# store in Google App Engine
|
8
|
+
module AppEngineBackend
|
9
|
+
mattr_accessor :base_url, :storage_prefix
|
10
|
+
@@base_url = "http://attachment-fu-gae.appspot.com"
|
11
|
+
@@storage_prefix = nil
|
12
|
+
|
13
|
+
def public_filename(thumbnail = nil)
|
14
|
+
thumbnails = HashWithIndifferentAccess.new(attachment_options[:thumbnails])
|
15
|
+
|
16
|
+
query = if thumbnail && thumbnails[thumbnail]
|
17
|
+
"?resize=#{thumbnails[thumbnail]}"
|
18
|
+
elsif thumbnail.is_a?(String)
|
19
|
+
"?resize=#{thumbnail}"
|
20
|
+
elsif attachment_options[:resize]
|
21
|
+
"?resize=#{attachment_options[:resize]}"
|
22
|
+
else
|
23
|
+
''
|
24
|
+
end
|
25
|
+
"#{AppEngineBackend.base_url}/#{full_filename}#{query}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_temp_file
|
29
|
+
write_to_temp_file current_data
|
30
|
+
end
|
31
|
+
|
32
|
+
# Gets the current data from the database
|
33
|
+
def current_data
|
34
|
+
uri = URI.parse(AppEngineBackend.base_url)
|
35
|
+
Net::HTTP.new(uri.host, uri.port).start do |http|
|
36
|
+
http.get(filename).response_body
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#TODO: HACK??
|
41
|
+
def process_attachment
|
42
|
+
@saved_attachment = true
|
43
|
+
end
|
44
|
+
|
45
|
+
# The full path to the file relative to the bucket name
|
46
|
+
# Example: <tt>:table_name/:id/:filename</tt>
|
47
|
+
def full_filename
|
48
|
+
['attachments', storage_prefix, attachment_options[:path_prefix].gsub('public/', ''), id.to_s, filename].compact.join('/')
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_or_update_thumbnail(*args)
|
52
|
+
#ignore
|
53
|
+
end
|
54
|
+
protected
|
55
|
+
def destroy_file
|
56
|
+
#ignore
|
57
|
+
end
|
58
|
+
|
59
|
+
def save_to_storage
|
60
|
+
if save_attachment?
|
61
|
+
response = MultipartPost.post(AppEngineBackend.base_url + '/attachments', [
|
62
|
+
{:name => 'uploaded_data', :value => temp_data, :mime_type => content_type, :filename => filename},
|
63
|
+
{:name => 'path', :value => full_filename}
|
64
|
+
])
|
65
|
+
raise response.body unless response.is_a? Net::HTTPRedirection
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: larsklevan-attachment_fu_app_engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lars Klevan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-02 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Extension for AttachmentFu (http://github.com/technoweenie/attachment_fu) which uses the Google App Engine for storage and image resizing.
|
17
|
+
email: tastybyte@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- app_engine/attachment_fu_app_engine/app.yaml
|
26
|
+
- app_engine/attachment_fu_app_engine/attachment.py
|
27
|
+
- app_engine/attachment_fu_app_engine/index.yaml
|
28
|
+
- app_engine/attachment_fu_app_engine/new.html
|
29
|
+
- app_engine/attachment_fu_app_engine/photo.pyc
|
30
|
+
- attachment_fu_app_engine.gemspec
|
31
|
+
- init.rb
|
32
|
+
- initializer.rb.tpl
|
33
|
+
- install.rb
|
34
|
+
- lib/multipart_post.rb
|
35
|
+
- lib/tasks/migrate.rake
|
36
|
+
- lib/technoweenie/attachment_fu/backends/app_engine_backend.rb
|
37
|
+
- MIT-LICENSE
|
38
|
+
- Rakefile
|
39
|
+
- README
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/larsklevan/attachment_fu_app_engine
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.2.0
|
63
|
+
signing_key:
|
64
|
+
specification_version: 2
|
65
|
+
summary: Extension for AttachmentFu which uses the Google App Engine for storage
|
66
|
+
test_files: []
|
67
|
+
|