larsklevan-attachment_fu_app_engine 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|