Changeset 568
- Timestamp:
- 06/30/08 09:33:00 (3 months ago)
- Files:
-
- thingfish/trunk (modified) (1 prop)
- thingfish/trunk/Rakefile (modified) (1 diff)
- thingfish/trunk/experiments/ab_extra_methods.patch (modified) (10 diffs)
- thingfish/trunk/lib/thingfish/daemon.rb (modified) (1 diff)
- thingfish/trunk/lib/thingfish/handler/default.rb (modified) (3 diffs)
- thingfish/trunk/lib/thingfish/request.rb (modified) (6 diffs)
- thingfish/trunk/plugins/thingfish-filter-mp3/lib/thingfish/filter/mp3.rb (modified) (6 diffs)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/data (added)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/data/APIC-1-image.mp3 (added)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/data/APIC-2-images.mp3 (added)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/data/PIC-1-image.mp3 (added)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/data/PIC-2-images.mp3 (added)
- thingfish/trunk/plugins/thingfish-filter-mp3/spec/thingfish/filter/mp3_spec.rb (modified) (3 diffs)
- thingfish/trunk/spec/thingfish/daemon_spec.rb (modified) (2 diffs)
- thingfish/trunk/spec/thingfish/handler/default_spec.rb (modified) (7 diffs)
- thingfish/trunk/spec/thingfish/multipartmimeparser_spec.rb (modified) (1 diff)
- thingfish/trunk/spec/thingfish/request_spec.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
thingfish/trunk
- Property svn:ignore changed from
coverage.info
*.log
*.tmp
pkg
coverage
benchmarks
commit-msg.txt
.rake_cache
TODO
to
coverage.info
*.log
*.tmp
pkg
coverage
benchmarks
commit-msg.txt
.rake_cache
TODO
tags
- Property svn:ignore changed from
thingfish/trunk/Rakefile
r561 r568 137 137 138 138 139 ### Task: generate ctags 140 ### This assumes exuberant ctags, since ctags 'native' doesn't support ruby anyway. 141 desc "Generate a ctags 'tags' file from ThingFish source" 142 task :ctags do 143 run %w{ ctags -R lib plugins misc } 144 end 145 146 139 147 ### Cruisecontrol task 140 148 desc "Cruisecontrol build" thingfish/trunk/experiments/ab_extra_methods.patch
r523 r568 1 1 2 This patch was submitted to Apache: 3 https://issues.apache.org/bugzilla/show_bug.cgi?id=44851 4 5 It adds PUT and DELETE support to the apache utility, 'ApacheBench'. 2 This patch is intended for ApacheBench version 2.3, which is distributed 3 with Apache 2.2.9. 6 4 7 5 8 --- ab.c.original 2008-04-21 08:46:05.000000000 -0700 9 +++ ab.c 2008-04-22 07:16:39.000000000 -0700 10 @@ -91,7 +91,7 @@ 6 --- ab.c.orig 2008-06-19 21:59:04.000000000 -0700 7 +++ ab.c 2008-06-19 22:44:25.000000000 -0700 8 @@ -84,6 +84,10 @@ 9 ** Version 2.3 10 ** SIGINT now triggers output_results(). 11 ** Contributed by colm, March 30, 2006 12 + ** 13 + ** Version 2.3.1 14 + ** PUT and DELETE HTTP method support. 15 + ** Contributed by Mahlon E. Smith <mahlon@martini.nu>, June 2008. 16 **/ 17 18 /* Note: this version string should start with \d+[\d\.]* and be a valid 19 @@ -95,7 +99,7 @@ 11 20 * ab - or to due to a change in the distribution it is compiled with 12 21 * (such as an APR change in for example blocking). 13 22 */ 14 -#define AP_AB_BASEREVISION "2. 0.40-dev"15 +#define AP_AB_BASEREVISION "2. 0.41-dev"23 -#define AP_AB_BASEREVISION "2.3" 24 +#define AP_AB_BASEREVISION "2.3.1" 16 25 17 26 /* 18 27 * BUGS: 19 @@ -254,7 +254,7 @@ 20 /* --------------------- GLOBALS ---------------------------- */ 28 @@ -261,7 +265,7 @@ 21 29 22 30 int verbosity = 0; /* no verbosity by default */ 31 int recverrok = 0; /* ok to proceed after socket receive errors */ 23 32 -int posting = 0; /* GET by default */ 24 +int method = 0; /* GET by default*/33 +int method = 0; /* -2 => DELETE, -1 => HEAD, 0 => GET (default), 1 => POST, 2 => PUT */ 25 34 int requests = 1; /* Number of requests to make */ 26 35 int heartbeatres = 100; /* How often do we say we're alive */ 27 36 int concurrency = 1; /* Number of multiple requests to make */ 28 @@ -6 21,7 +621,7 @@37 @@ -631,7 +635,7 @@ 29 38 c->connect = tnow; 39 c->rwrote = 0; 30 40 c->rwrite = reqlen; 31 c->rwrote = 0;32 41 - if (posting) 33 42 + if (method) … … 35 44 } 36 45 else if (tnow > c->connect + aprtimeout) { 37 @@ -7 61,8 +761,10 @@46 @@ -757,8 +761,10 @@ 38 47 if (keepalive) 39 printf("Keep-Alive requests: % ld\n", doneka);40 printf("Total transferred: % ldbytes\n", totalread);48 printf("Keep-Alive requests: %d\n", doneka); 49 printf("Total transferred: %" APR_INT64_T_FMT " bytes\n", totalread); 41 50 - if (posting > 0) 42 51 + if (method == 1) 43 printf("Total POSTed: % ld\n", totalposted);52 printf("Total POSTed: %" APR_INT64_T_FMT "\n", totalposted); 44 53 + if (method == 2) 45 + printf("Total PUT: % ld\n", totalposted);46 printf("HTML transferred: % ldbytes\n", totalbread);54 + printf("Total PUT: %" APR_INT64_T_FMT "\n", totalposted); 55 printf("HTML transferred: %" APR_INT64_T_FMT " bytes\n", totalbread); 47 56 48 57 /* avoid divide by zero */ 49 @@ -77 5,7 +777,7 @@50 (float) (1000 * timetaken / done));58 @@ -771,7 +777,7 @@ 59 (double) timetaken * 1000 / done); 51 60 printf("Transfer rate: %.2f [Kbytes/sec] received\n", 52 (float) (totalread / 1024 / timetaken));61 (double) totalread / 1024 / timetaken); 53 62 - if (posting > 0) { 54 63 + if (method > 0) { 55 64 printf(" %.2f kb/s sent\n", 56 ( float) (totalposted / timetaken / 1024));65 (double) totalposted / timetaken / 1024); 57 66 printf(" %.2f kb/s total\n", 58 @@ -10 30,10 +1032,14 @@67 @@ -1042,10 +1048,14 @@ 59 68 printf("<tr %s><th colspan=2 %s>Total transferred:</th>" 60 "<td colspan=2 %s>% ldbytes</td></tr>\n",69 "<td colspan=2 %s>%" APR_INT64_T_FMT " bytes</td></tr>\n", 61 70 trstring, tdstring, tdstring, totalread); 62 71 - if (posting > 0) 63 72 + if (method == 1) 64 73 printf("<tr %s><th colspan=2 %s>Total POSTed:</th>" 65 "<td colspan=2 %s>% ld</td></tr>\n",74 "<td colspan=2 %s>%" APR_INT64_T_FMT "</td></tr>\n", 66 75 trstring, tdstring, tdstring, totalposted); 67 76 + if (method == 2) 68 77 + printf("<tr %s><th colspan=2 %s>Total PUT:</th>" 69 + "<td colspan=2 %s>% ld</td></tr>\n",78 + "<td colspan=2 %s>%" APR_INT64_T_FMT "</td></tr>\n", 70 79 + trstring, tdstring, tdstring, totalposted); 71 80 printf("<tr %s><th colspan=2 %s>HTML transferred:</th>" 72 "<td colspan=2 %s>% ldbytes</td></tr>\n",81 "<td colspan=2 %s>%" APR_INT64_T_FMT " bytes</td></tr>\n", 73 82 trstring, tdstring, tdstring, totalbread); 74 @@ -10 46,7 +1052,7 @@83 @@ -1058,7 +1068,7 @@ 75 84 printf("<tr %s><th colspan=2 %s>Transfer rate:</th>" 76 85 "<td colspan=2 %s>%.2f kb/s received</td></tr>\n", 77 trstring, tdstring, tdstring, ( float) (totalread)/ timetaken);86 trstring, tdstring, tdstring, (double) totalread / timetaken); 78 87 - if (posting > 0) { 79 88 + if (method > 0) { … … 81 90 "<td colspan=2 %s>%.2f kb/s sent</td></tr>\n", 82 91 trstring, tdstring, tdstring, 83 @@ -1480,6 +1486,7 @@ 84 85 static void test(void) 86 { 87 + char *verb = "GET"; 88 apr_time_t now; 89 apr_int16_t rv; 90 long i; 91 @@ -1543,24 +1550,46 @@ 92 @@ -1466,8 +1476,8 @@ 93 cl = strstr(c->cbuff, "Content-length:"); 94 if (cl) { 95 c->keepalive = 1; 96 - /* response to HEAD doesn't have entity body */ 97 - c->length = posting >= 0 ? atoi(cl + 16) : 0; 98 + /* response to HEAD and DELETE doesn't have entity body */ 99 + c->length = method >= 0 ? atoi(cl + 16) : 0; 100 } 101 /* The response may not have a Content-Length header */ 102 if (!cl) { 103 @@ -1532,6 +1542,7 @@ 104 int i; 105 apr_status_t status; 106 int snprintf_res = 0; 107 + char *verb = "GET"; 108 #ifdef NOT_ASCII 109 apr_size_t inbytes_left, outbytes_left; 110 #endif 111 @@ -1588,24 +1599,44 @@ 92 112 } 93 113 … … 96 116 + if (method <= 0) { 97 117 + switch (method) { 98 + case -2: 99 + verb = "DELETE"; 100 + break; 101 + case -1: 102 + verb = "HEAD"; 103 + break; 104 + case 0: 105 + verb = "GET"; 106 + break; 107 + } 108 + 118 + case -2: 119 + verb = "DELETE"; 120 + break; 121 + case -1: 122 + verb = "HEAD"; 123 + break; 124 + case 0: 125 + verb = "GET"; 126 + break; 127 + } 109 128 snprintf_res = apr_snprintf(request, sizeof(_request), 110 129 "%s %s HTTP/1.0\r\n" … … 119 138 else { 120 139 + switch (method) { 121 + case 1: 122 + verb = "POST"; 123 + break; 124 + case 2: 125 + verb = "PUT"; 126 + break; 127 + } 128 + 140 + case 1: 141 + verb = "POST"; 142 + break; 143 + case 2: 144 + verb = "PUT"; 145 + break; 146 + } 129 147 snprintf_res = apr_snprintf(request, sizeof(_request), 130 148 - "POST %s HTTP/1.0\r\n" … … 135 153 "%s" 136 154 "\r\n", 137 + verb,155 + verb, 138 156 (isproxy) ? fullurl : path, 139 157 keepalive ? "Connection: Keep-Alive\r\n" : "", 140 158 cookie, auth, 141 @@ -1 577,9 +1606,9 @@159 @@ -1622,9 +1653,9 @@ 142 160 reqlen = strlen(request); 143 161 … … 151 169 if (!buff) { 152 170 fprintf(stderr, "error creating request buffer: out of memory\n"); 153 @@ -1779,10 +1808,12 @@ 154 fprintf(stderr, " -c concurrency Number of multiple requests to make\n"); 171 @@ -1825,12 +1856,14 @@ 155 172 fprintf(stderr, " -t timelimit Seconds to max. wait for responses\n"); 156 fprintf(stderr, " -p postfile File containing data to POST\n"); 157 + fprintf(stderr, " -u putfile File containing data to PUT\n"); 158 fprintf(stderr, " -T content-type Content-type header for POSTing\n"); 173 fprintf(stderr, " -b windowsize Size of TCP send/receive buffer, in bytes\n"); 174 fprintf(stderr, " -p postfile File containing data to POST. Remember also to set -T\n"); 175 + fprintf(stderr, " -u putfile File containing data to PUT.\n"); 176 fprintf(stderr, " -T content-type Content-type header for POSTing, eg.\n"); 177 fprintf(stderr, " 'application/x-www-form-urlencoded'\n"); 178 fprintf(stderr, " Default is 'text/plain'\n"); 159 179 fprintf(stderr, " -v verbosity How much troubleshooting info to print\n"); 160 180 fprintf(stderr, " -w Print out results in HTML tables\n"); … … 165 185 fprintf(stderr, " -y attributes String to insert as tr attributes\n"); 166 186 fprintf(stderr, " -z attributes String to insert as td or th attributes\n"); 167 @@ -1962,7 +1993,7 @@ 187 @@ -1931,7 +1964,7 @@ 188 189 /* ------------------------------------------------------- */ 190 191 -/* read data to POST from file, save contents and length */ 192 +/* read data to POST/PUT from file, save contents and length */ 193 194 static int open_postfile(const char *pfile) 195 { 196 @@ -1942,26 +1975,26 @@ 197 198 rv = apr_file_open(&postfd, pfile, APR_READ, APR_OS_DEFAULT, cntxt); 199 if (rv != APR_SUCCESS) { 200 - fprintf(stderr, "ab: Could not open POST data file (%s): %s\n", pfile, 201 + fprintf(stderr, "ab: Could not open data file (%s): %s\n", pfile, 202 apr_strerror(rv, errmsg, sizeof errmsg)); 203 return rv; 204 } 205 206 rv = apr_file_info_get(&finfo, APR_FINFO_NORM, postfd); 207 if (rv != APR_SUCCESS) { 208 - fprintf(stderr, "ab: Could not stat POST data file (%s): %s\n", pfile, 209 + fprintf(stderr, "ab: Could not stat data file (%s): %s\n", pfile, 210 apr_strerror(rv, errmsg, sizeof errmsg)); 211 return rv; 212 } 213 postlen = (apr_size_t)finfo.size; 214 postdata = malloc(postlen); 215 if (!postdata) { 216 - fprintf(stderr, "ab: Could not allocate POST data buffer\n"); 217 + fprintf(stderr, "ab: Could not allocate data buffer\n"); 218 return APR_ENOMEM; 219 } 220 rv = apr_file_read_full(postfd, postdata, postlen, NULL); 221 if (rv != APR_SUCCESS) { 222 - fprintf(stderr, "ab: Could not read POST data file: %s\n", 223 + fprintf(stderr, "ab: Could not read data file: %s\n", 224 apr_strerror(rv, errmsg, sizeof errmsg)); 225 return rv; 226 } 227 @@ -2016,7 +2049,7 @@ 168 228 #endif 169 229 170 230 apr_getopt_init(&opt, cntxt, argc, argv); 171 - while ((status = apr_getopt(opt, "n:c:t: T:p:v:kVhwix:y:z:C:H:P:A:g:X:de:Sq"172 + while ((status = apr_getopt(opt, "n:c:t: T:p:v:kVhwix:y:z:C:H:P:A:g:X:de:Sq:Du:"231 - while ((status = apr_getopt(opt, "n:c:t:b:T:p:v:rkVhwix:y:z:C:H:P:A:g:X:de:Sq" 232 + while ((status = apr_getopt(opt, "n:c:t:b:T:p:v:rkVhwix:y:z:C:H:P:A:g:X:de:SqDu:" 173 233 #ifdef USE_SSL 174 234 "Z:f:" 175 235 #endif 176 @@ - 1984,9 +2015,12 @@177 concurrency= atoi(optarg);236 @@ -2041,9 +2074,12 @@ 237 windowsize = atoi(optarg); 178 238 break; 179 239 case 'i': … … 190 250 case 'g': 191 251 gnuplot = strdup(optarg); 192 @@ -20 01,10 +2035,20 @@252 @@ -2058,10 +2094,20 @@ 193 253 confidence = 0; 194 254 break; … … 214 274 else if (postdata) { 215 275 exit(r); 216 thingfish/trunk/lib/thingfish/daemon.rb
r560 r568 214 214 215 215 # Store some default metadata about the resource 216 @metastore.transaction do 217 now = Time.now 218 metadata = @metastore[ uuid ] 219 metadata.update( body_metadata ) 220 221 # :BRANCH: Won't overwrite an existing creation date 222 metadata.checksum = checksum 223 metadata.created = now unless metadata.created 224 metadata.modified = now 225 226 self.log.info "Created new resource %s (%s, %0.2f KB), checksum is %s" % [ 227 uuid, 228 metadata.format, 229 Integer(metadata.extent) / 1024.0, 230 metadata.checksum 231 ] 216 begin 217 @metastore.transaction do 218 now = Time.now 219 metadata = @metastore[ uuid ] 220 metadata.update( body_metadata ) 221 222 # :BRANCH: Won't overwrite an existing creation date 223 metadata.checksum = checksum 224 metadata.created = now unless metadata.created 225 metadata.modified = now 226 227 self.log.info "Created new resource %s (%s, %0.2f KB), checksum is %s" % [ 228 uuid, 229 metadata.format, 230 Integer(metadata.extent) / 1024.0, 231 metadata.checksum 232 ] 233 end 234 rescue => err 235 self.log.error "Error saving resource to metastore: %s" % [ err.message ] 236 @filestore.delete( uuid ) if new_resource 237 raise 232 238 end 233 239 thingfish/trunk/lib/thingfish/handler/default.rb
r560 r568 243 243 def handle_create_request( request, response ) 244 244 245 if request. entity_bodies.length > 1246 self.log.error "Can't handle multipart request (%p)" % [ request. entity_bodies ]245 if request.bodies.length > 1 246 self.log.error "Can't handle multipart request (%p)" % [ request.bodies ] 247 247 raise ThingFish::NotImplementedError, "multipart upload not implemented" 248 248 end … … 251 251 252 252 # Store the primary resource 253 body, metadata = request. entity_bodies.to_a.flatten253 body, metadata = request.bodies.to_a.flatten 254 254 uuid = self.daemon.store_resource( body, metadata ) 255 255 … … 271 271 def handle_update_uuid_request( request, response, uuid ) 272 272 273 if request.bodies.length > 1 274 self.log.error "Can't handle multipart request" % [ request.bodies ] 275 raise ThingFish::NotImplementedError, "multipart upload not implemented" 276 end 277 273 278 # :TODO: Handle slow/big uploads by returning '202 Accepted' and spawning 274 279 # a handler thread? 275 280 new_resource = ! @filestore.has_file?( uuid ) 276 body, metadata = request. get_body_and_metadata281 body, metadata = request.bodies.to_a.flatten 277 282 self.daemon.store_resource( body, metadata, uuid ) 278 283 thingfish/trunk/lib/thingfish/request.rb
r560 r568 261 261 262 262 263 ### Attach additional body and metadata information to the primary 264 ### body, that will be stored with related_to metakeys. 263 ### Attach additional resources and metadata information to the primary 264 ### resource body; these will be stored with `related_to` metadata pointing 265 ### to the original. 265 266 ### 266 ### + body+::267 ### The uploaded (primary) body268 ### +related_ body+::267 ### +resource+:: 268 ### The uploaded (primary) resource body 269 ### +related_resource+:: 269 270 ### The new resource body as an IO-like object 270 271 ### +related_metadata+:: … … 306 307 307 308 308 ### Returns the entity bodies of the request along with any related metadata as309 ### a Hash:310 ### {311 ### <body io> => { <body metadata> },312 ### ...313 ### }314 def entity_bodies315 # Parse the request's body parts if they aren't already316 unless @entity_bodies317 if self.has_multipart_body?318 self.log.debug "Parsing multiple entity bodies."319 @entity_bodies, @form_metadata = self.parse_multipart_body320 else321 self.log.debug "Parsing single entity body."322 body, metadata = self.get_body_and_metadata323 324 @entity_bodies = { body => metadata }325 @form_metadata = {}326 end327 328 self.log.debug "Parsed %d bodies and %d form_metadata (%p)" %329 [@entity_bodies.length, @form_metadata.length, @form_metadata.keys]330 end331 332 return @entity_bodies333 end334 335 336 309 ### Call the provided block once for each entity body of the request, which may 337 ### be multiple times in the case of a multipart request. If +include_appended _resources+310 ### be multiple times in the case of a multipart request. If +include_appended+ 338 311 ### is +true+, any resources which have been appended will be yielded immediately after the 339 312 ### body to which they are related. Note that this applies even in the current loop -- the … … 360 333 361 334 335 ### Fetch a flat hash of the entity bodies for the request. If +include_appended+ is +true+, 336 ### include any appended related resources. 337 def bodies( include_appended=false ) 338 rval = {} 339 340 self.each_body( include_appended ) do |body, metadata| 341 rval[body] = metadata 342 end 343 344 return rval 345 end 346 347 362 348 ### Check the body IO objects to ensure they're still open. 363 349 def check_body_ios … … 377 363 @entity_bodies[ clone ] = @entity_bodies.delete( body ) if @entity_bodies.key?( body ) 378 364 @related_resources[ clone ] = @related_resources.delete( body ) if @related_resources.key?( body ) 365 @metadata[ clone ] = @metadata.delete( body ) if @metadata.key?( body ) 379 366 @related_resources.each do |_,hash| 380 367 hash[ clone ] = hash.delete( body ) if hash.key?( body ) … … 499 486 ######### 500 487 501 ### Get the body IO and the merged hash of metadata 502 def get_body_and_metadata 488 ### Returns the entity bodies of the request along with any related metadata as 489 ### a Hash: 490 ### { 491 ### <body io> => { <body metadata> }, 492 ### ... 493 ### } 494 def entity_bodies 495 # Parse the request's body parts if they aren't already 496 unless @entity_bodies 497 if self.has_multipart_body? 498 self.log.debug "Parsing multiple entity bodies." 499 @entity_bodies, @form_metadata = self.parse_multipart_body 500 else 501 self.log.debug "Parsing single entity body." 502 body, metadata = self.parse_singlepart_body 503 504 @entity_bodies = { body => metadata } 505 @form_metadata = {} 506 end 507 508 self.log.debug "Parsed %d bodies and %d form_metadata (%p)" % 509 [@entity_bodies.length, @form_metadata.length, @form_metadata.keys] 510 end 511 512 return @entity_bodies 513 end 514 515 516 ### Get the body IO and the initial metadata from a non-multipart request 517 def parse_singlepart_body 503 518 raise ArgumentError, "Can't return a single body for a multipart request" if 504 519 self.has_multipart_body? 505 520 506 default_metadata = {521 metadata = { 507 522 :useragent => self.headers[ :user_agent ], 508 :uploadaddress => self.remote_addr 523 :extent => self.headers[ :content_length ], 524 :uploadaddress => self.remote_addr, 525 :format => self.content_type 509 526 } 510 527 … … 512 529 if self.headers[:content_disposition] && 513 530 self.headers[:content_disposition] =~ /filename="(?:.*\\)?(.+?)"/i 514 default_metadata[ :title ] = $1 515 end 516 517 extracted_metadata = self.metadata[ @mongrel_request.body ] || {} 518 519 # Content metadata is determined from http headers 520 merged = extracted_metadata.merge({ 521 :format => self.content_type, 522 :extent => self.headers[ :content_length ], 523 }) 524 merged.update( default_metadata ) 525 526 return @mongrel_request.body, merged 531 metadata[ :title ] = $1 532 end 533 534 return @mongrel_request.body, metadata 527 535 end 528 536 thingfish/trunk/plugins/thingfish-filter-mp3/lib/thingfish/filter/mp3.rb
r497 r568 24 24 # Please see the LICENSE file for licensing details. 25 25 # 26 26 27 27 28 begin … … 56 57 NULL = "\x0" 57 58 59 # Attached picture "PIC" 60 # Frame size $xx xx xx 61 # ---- mp3info is 'helpfully' cropping out frame size. 62 # Text encoding $xx 63 # Image format $xx xx xx 64 # Picture type $xx 65 # Description <textstring> $00 (00) 66 # Picture data <binary data> 67 PIC_FORMAT = 'h a3 h Z* a*' 68 69 # Attached picture "APIC" 70 # Text encoding $xx 71 # MIME type <text string> $00 72 # Picture type $xx 73 # Description <text string according to encoding> $00 (00) 74 # Picture data <binary data> 75 APIC_FORMAT = 'h Z* h Z* a*' 76 58 77 # The Array of types this filter is interested in 59 78 HANDLED_TYPES = [ ThingFish::AcceptParam.parse('audio/mpeg') ] … … 83 102 request.each_body do |body, metadata| 84 103 if self.accept?( metadata[:format] ) 85 mp3_metadata = self.extract_id3_metadata( body ) 104 105 id3 = Mp3Info.new( body.path ) 106 mp3_metadata = self.extract_id3_metadata( id3 ) 107 108 # Append any album images as related resources 109 self.extract_images( id3 ).each do |io, metadata| 110 request.append_related_resource( body, io, metadata ) 111 end 112 86 113 request.append_metadata_for( body, mp3_metadata ) 87 114 self.log.debug "Extracted mp3 info: %p" % [ mp3_metadata.keys ] … … 132 159 ######### 133 160 134 ### Extract and normalize ID3 metadata from the MPEG layer 3 audio in the given +io+ 135 ### object and return it as a hash. 136 def extract_id3_metadata( io ) 137 info = Mp3Info.new( io.path ) 161 ### Normalize metadata from the MP3Info object and return it as a hash. 162 def extract_id3_metadata( id3 ) 138 163 mp3_metadata = { 139 :mp3_frequency => i nfo.samplerate,140 :mp3_bitrate => i nfo.bitrate,141 :mp3_vbr => i nfo.vbr,142 :mp3_title => i nfo.tag.title,143 :mp3_artist => i nfo.tag.artist,144 :mp3_album => i nfo.tag.album,145 :mp3_year => i nfo.tag.year,146 :mp3_genre => i nfo.tag.genre,147 :mp3_tracknum => i nfo.tag.tracknum,148 :mp3_comments => i nfo.tag.comments,164 :mp3_frequency => id3.samplerate, 165 :mp3_bitrate => id3.bitrate, 166 :mp3_vbr => id3.vbr, 167 :mp3_title => id3.tag.title, 168 :mp3_artist => id3.tag.artist, 169 :mp3_album => id3.tag.album, 170 :mp3_year => id3.tag.year, 171 :mp3_genre => id3.tag.genre, 172 :mp3_tracknum => id3.tag.tracknum, 173 :mp3_comments => id3.tag.comments, 149 174 } 150 175 151 176 # ID3V2 2.2.0 has three-letter tags, so map those if the artist info isn't set 152 if info.hastag2? && mp3_metadata[:mp3_artist].nil? 153 self.log.debug " extracting old-style ID3v2 info" % [info.tag2.version] 177 if id3.hastag2? 178 if mp3_metadata[:mp3_artist].nil? 179 self.log.debug " extracting old-style ID3v2 info" % [id3.tag2.version] 154 180 155 mp3_metadata.merge!({ 156 :mp3_title => info.tag2.TT2, 157 :mp3_artist => info.tag2.TP1, 158 :mp3_album => info.tag2.TAL, 159 :mp3_year => info.tag2.TYE, 160 :mp3_tracknum => info.tag2.TRK, 161 :mp3_comments => info.tag2.COM, 162 :mp3_genre => info.tag2.TCO, 163 }) 164 end 165 166 if info.hastag2? 167 info.tag2.keys.inject( mp3_metadata ) do |metadata, key| 168 nkey = 'mp3_id3v2_' + key.to_s.downcase.gsub( /\W+/, '_' ) 169 metadata[ nkey.to_sym ] = info.tag2[ key ] 170 metadata 181 mp3_metadata.merge!({ 182 :mp3_title => id3.tag2.TT2, 183 :mp3_artist => id3.tag2.TP1, 184 :mp3_album => id3.tag2.TAL, 185 :mp3_year => id3.tag2.TYE, 186 :mp3_tracknum => id3.tag2.TRK, 187 :mp3_comments => id3.tag2.COM, 188 :mp3_genre => id3.tag2.TCO, 189 }) 171 190 end 172 191 end … … 174 193 return sanitize_values( mp3_metadata ) 175 194 end 195 196 197 ### Extract image data from id3 information, supports both APIC (2.3) and the older style 198 ### PIC (2.2). Return value is a hash with IO keys and mimetype values. 199 ### { 200 ### io => { format => 'image/jpeg' } 201 ### io2 => { format => 'image/jpeg' } 202 ### } 203 def extract_images( id3 ) 204 data = {} 205 return data unless id3.hastag2? 206 207 if id3.tag2.APIC 208 images = [ id3.tag2.APIC ].flatten 209 images.each do |img| 210 blob, mime = img.unpack( APIC_FORMAT ).values_at( 4, 1 ) 211 data[ StringIO.new(blob) ] = { 212 :format => mime, 213 :extent => blob.length, 214 :title => 'album-art' 215 } 216 end 217 218 elsif id3.tag2.PIC 219 images = [ id3.tag2.PIC ].flatten 220 images.each do |img| 221 blob, type = img.unpack( PIC_FORMAT ).values_at( 4, 1 ) 222 mime = MIMETYPE_MAP[ ".#{type.downcase}" ] or next 223 data[ StringIO.new(blob) ] = { 224 :format => mime, 225 :extent => blob.length, 226 :title => 'album-art' 227 } 228 end 229 end 230 231 return data 232 end 233 176 234 177 235 … … 199 257 end 200 258 201 202 203 259 end # class ThingFish::Mp3Filter 204 260 205 261 # vim: set nosta noet ts=4 sw=4: 206 207 thingfish/trunk/plugins/thingfish-filter-mp3/spec/thingfish/filter/mp3_spec.rb
r496 r568 54 54 } 55 55 56 MP3_SPECDIR = Pathname.new( __FILE__ ).dirname.parent.parent 57 MP3_DATADIR = MP3_SPECDIR + 'data' 58 59 JPEG_MAGIC_REGEXP = /^\377\330\377\340\000\020JFIF/ 60 PNG_MAGIC_REGEXP = /^\x89PNG/ 61 56 62 57 63 before( :each ) do 58 64 @filter = ThingFish::Filter.create( 'mp3' ) 59 65 60 @io = StringIO.new( TEST_CONTENT )61 @io.stub!( :path ).and_return( :a_dummy_path )62 66 @response = stub( "response object" ) 63 64 67 @request_metadata = { :format => 'audio/mpeg' } 65 68 @request = mock( "request object" ) 66 @request.stub!( :http_method ).and_return( 'POST' )67 @request.stub!( :each_body ).and_yield( @io, @request_metadata )68 69 @mp3info = mock( "MP3 info object", :null_object => true )70 Mp3Info.stub!( :new ).and_return( @mp3info )71 @id3tag = mock( "ID3 tag object", :null_object => true )72 @mp3info.stub!( :tag ).and_return( @id3tag )73 69 end 74 70 … … 76 72 ### Shared behaviors 77 73 it_should_behave_like "A Filter" 78 79 80 ### Filter-specific tests 81 82 it "extracts MP3 metadata from ID3v1 tags of uploaded MP3s" do 83 @mp3info.should_receive( :samplerate ).and_return( 44000 ) 84 @mp3info.should_receive( :bitrate ).and_return( 128 ) 85 @mp3info.should_receive( :vbr ).and_return( true ) 86 87 @id3tag.should_receive( :tracknum ).and_return( TEST_TRACKNUM ) 88 @id3tag.should_receive( :title ).and_return( TEST_MP3_TITLE ) 89 @id3tag.should_receive( :artist ).and_return( TEST_ARTIST ) 90 @id3tag.should_receive( :album ).and_return( TEST_ALBUM ) 91 @id3tag.should_receive( :comments ).and_return( TEST_COMMENTS ) 92 @id3tag.should_receive( :year ).and_return( TEST_YEAR ) 93 @id3tag.should_receive( :genre ).and_return( TEST_GENRE ) 94 95 @request.should_receive( :append_metadata_for ).with( @io, EXTRACTED_METADATA ) 96 @filter.handle_request( @request, @response ) 97 end 98 99 100 it "extracts MP3 metadata from ID3v2 (v2.2.0) tags of uploaded MP3s" do 101 extracted_metadata = {} 102 v2tag = mock( "ID3v2 tag", :null_object => true ) 103 104 @mp3info.should_receive( :samplerate ).and_return( 44000 ) 105 @mp3info.should_receive( :bitrate ).and_return( 128 ) 106 @mp3info.should_receive( :vbr ).and_return( true ) 107 108 @id3tag.should_receive( :title ).and_return( nil ) 109 @id3tag.should_receive( :artist ).and_return( nil ) 110 @id3tag.should_receive( :album ).and_return( nil ) 111 @id3tag.should_receive( :year ).and_return( nil ) 112 @id3tag.should_receive( :tracknum ).and_return( nil ) 113 @id3tag.should_receive( :comments ).and_return( nil ) 114 @id3tag.should_receive( :genre ).and_return( nil ) 115 116 @mp3info.should_receive( :hastag2? ). 117 at_least( :once ). 118 and_return( true ) 119 @mp3info.should_receive( :tag2 ). 120 at_least( :once ). 121 and_return( v2tag ) 122 123 v2tag.should_receive(:TT2).and_return( TEST_MP3_TITLE ) 124 v2tag.should_receive(:TP1).and_return( TEST_ARTIST ) 125 v2tag.should_receive(:TAL).and_return( TEST_ALBUM ) 126 v2tag.should_receive(:TYE).and_return( TEST_YEAR ) 127 v2tag.should_receive(:TRK).and_return( TEST_TRACKNUM ) 128 v2tag.should_receive(:COM).and_return( TEST_COMMENTS ) 129 v2tag.should_receive(:TCO).and_return( TEST_GENRE ) 130 131 @request.should_receive( :append_metadata_for ).with( @io, EXTRACTED_METADATA ) 132 @filter.handle_request( @request, @response ) 133 end 134 135 136 it "ignores all non-mp3 uploads" do 137 @request_metadata[ :format ] = 'dessert/tapioca' 138 Mp3Info.should_not_receive( :new ) 139 @request.should_not_receive( :metadata ) 140 141 @filter.handle_request( @request, @response ) 142 end 143 144 145 it "normalizes id3 values" do 146 @mp3info.should_receive( :samplerate ).and_return( 44000 ) 147 @mp3info.should_receive( :bitrate ).and_return( 128 ) 148 @mp3info.should_receive( :vbr ).and_return( true ) 149 150 @id3tag.should_receive( :tracknum ).and_return( TEST_TRACKNUM ) 151 @id3tag.should_receive( :year ).and_return( TEST_YEAR ) 152 @id3tag.should_receive( :genre ).and_return( TEST_GENRE ) 153 154 @id3tag.should_receive( :title ).and_return( TEST_MP3_TITLE + "\x0" ) 155 @id3tag.should_receive( :artist ).and_return( "\n" + TEST_ARTIST + " \n\n" ) 156 @id3tag.should_receive( :album ).and_return( nil ) 157 @id3tag.should_receive( :comments ).and_return([ 158 TEST_COMMENTS[0] + "\x0", 159 " " + TEST_COMMENTS[1] + "\n\n", 160 TEST_COMMENTS[2] 161 ]) 162 163 # The nil should be transformed into an '(unknown)', but everything else should 164 # be the same 165 normalized_values = EXTRACTED_METADATA.dup 166 normalized_values[:mp3_album] = "(unknown)" 167 168 @request.should_receive( :append_metadata_for ).with( @io, normalized_values ) 169 @filter.handle_request( @request, @response ) 170 end 171 172 74 173 75 it "ignores non-POST requests" do 174 76 @request.should_receive( :http_method ). … … 179 81 @filter.handle_request( @request, @response ) 180 82 end 181 83 84 85 ### Filter-specific tests 86 87 describe ' given an mp3 that has id3 data' do 88 89 before( :each ) do 90 @io = StringIO.new( TEST_CONTENT ) 91 @io.stub!( :path ).and_return( :a_dummy_path ) 92 93 @request.stub!( :http_method ).and_return( 'POST' ) 94 @request.stub!( :each_body ).and_yield( @io, @request_metadata ) 95 96 @mp3info = mock( "MP3 info object", :null_object => true ) 97 Mp3Info.stub!( :new ).and_return( @mp3info ) 98 @id3tag = mock( "ID3 tag object", :null_object => true ) 99 @mp3info.stub!( :tag ).and_return( @id3tag ) 100 end 101 102 it "extracts MP3 metadata from ID3v1 tags of uploaded MP3s" do 103 @mp3info.should_receive( :samplerate ).and_return( 44000 ) 104 @mp3info.should_receive( :bitrate ).and_return( 128 ) 105 @mp3info.should_receive( :vbr ).and_return( true ) 106 107 @id3tag.should_receive( :tracknum ).and_return( TEST_TRACKNUM ) 108 @id3tag.should_receive( :title ).and_return( TEST_MP3_TITLE ) 109 @id3tag.should_receive( :artist ).and_return( TEST_ARTIST ) 110 @id3tag.should_receive( :album ).and_return( TEST_ALBUM ) 111 @id3tag.should_receive( :comments ).and_return( TEST_COMMENTS ) 112 @id3tag.should_receive( :year ).and_re
