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.
@@ -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
@@ -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,8 @@
1
+ application: attachment-fu-gae
2
+ version: 1
3
+ runtime: python
4
+ api_version: 1
5
+
6
+ handlers:
7
+ - url: /.*
8
+ script: attachment.py
@@ -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>
@@ -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
@@ -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) %>"
@@ -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
+