Changeset 568

Show
Ignore:
Timestamp:
06/30/08 09:33:00 (3 months ago)
Author:
mahlon
Message:
  • Add a Rake task for generating tags, using exhuberant ctags.
  • Extract album art from mp3 files.
  • Prune unwanted id3 tags from mp3s
  • Make the default handler use the new ThingFish::Request API.
  • Restructure ThingFish::Request's public interface to request bodies.
  • Update the ApacheBench? patch (PUT and DELETE support) for Apache 2.2.9.
  • "Rollback" file storage on the event of a metadata store error.
Files:

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
  • thingfish/trunk/Rakefile

    r561 r568  
    137137 
    138138 
     139### Task: generate ctags 
     140### This assumes exuberant ctags, since ctags 'native' doesn't support ruby anyway. 
     141desc "Generate a ctags 'tags' file from ThingFish source" 
     142task :ctags do 
     143    run %w{ ctags -R lib plugins misc } 
     144end 
     145 
     146 
    139147### Cruisecontrol task 
    140148desc "Cruisecontrol build" 
  • thingfish/trunk/experiments/ab_extra_methods.patch

    r523 r568  
    11 
    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'. 
     2This patch is intended for ApacheBench version 2.3, which is distributed 
     3with Apache 2.2.9. 
    64 
    75 
    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 @@ 
    1120  * ab - or to due to a change in the distribution it is compiled with 
    1221  * (such as an APR change in for example blocking). 
    1322  */ 
    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
    1625  
    1726 /* 
    1827  * BUGS: 
    19 @@ -254,7 +254,7 @@ 
    20  /* --------------------- GLOBALS ---------------------------- */ 
     28@@ -261,7 +265,7 @@ 
    2129  
    2230 int verbosity = 0;      /* no verbosity by default */ 
     31 int recverrok = 0;      /* ok to proceed after socket receive errors */ 
    2332-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 */ 
    2534 int requests = 1;       /* Number of requests to make */ 
    2635 int heartbeatres = 100; /* How often do we say we're alive */ 
    2736 int concurrency = 1;    /* Number of multiple requests to make */ 
    28 @@ -621,7 +621,7 @@ 
     37@@ -631,7 +635,7 @@ 
    2938             c->connect = tnow; 
     39             c->rwrote = 0; 
    3040             c->rwrite = reqlen; 
    31              c->rwrote = 0; 
    3241-            if (posting) 
    3342+            if (method) 
     
    3544         } 
    3645         else if (tnow > c->connect + aprtimeout) { 
    37 @@ -761,8 +761,10 @@ 
     46@@ -757,8 +761,10 @@ 
    3847     if (keepalive) 
    39          printf("Keep-Alive requests:    %ld\n", doneka); 
    40      printf("Total transferred:      %ld bytes\n", totalread); 
     48         printf("Keep-Alive requests:    %d\n", doneka); 
     49     printf("Total transferred:      %" APR_INT64_T_FMT " bytes\n", totalread); 
    4150-    if (posting > 0) 
    4251+    if (method == 1) 
    43          printf("Total POSTed:           %ld\n", totalposted); 
     52         printf("Total POSTed:           %" APR_INT64_T_FMT "\n", totalposted); 
    4453+    if (method == 2) 
    45 +        printf("Total PUT:              %ld\n", totalposted); 
    46      printf("HTML transferred:       %ld bytes\n", totalbread); 
     54+        printf("Total PUT:              %" APR_INT64_T_FMT "\n", totalposted); 
     55     printf("HTML transferred:       %" APR_INT64_T_FMT " bytes\n", totalbread); 
    4756  
    4857     /* avoid divide by zero */ 
    49 @@ -775,7 +777,7 @@ 
    50             (float) (1000 * timetaken / done)); 
     58@@ -771,7 +777,7 @@ 
     59                (double) timetaken * 1000 / done); 
    5160         printf("Transfer rate:          %.2f [Kbytes/sec] received\n", 
    52             (float) (totalread / 1024 / timetaken)); 
     61                (double) totalread / 1024 / timetaken); 
    5362-        if (posting > 0) { 
    5463+        if (method > 0) { 
    5564             printf("                        %.2f kb/s sent\n", 
    56                 (float) (totalposted / timetaken / 1024)); 
     65                (double) totalposted / timetaken / 1024); 
    5766             printf("                        %.2f kb/s total\n", 
    58 @@ -1030,10 +1032,14 @@ 
     67@@ -1042,10 +1048,14 @@ 
    5968     printf("<tr %s><th colspan=2 %s>Total transferred:</th>" 
    60         "<td colspan=2 %s>%ld bytes</td></tr>\n", 
     69        "<td colspan=2 %s>%" APR_INT64_T_FMT " bytes</td></tr>\n", 
    6170        trstring, tdstring, tdstring, totalread); 
    6271-    if (posting > 0) 
    6372+    if (method == 1) 
    6473         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", 
    6675            trstring, tdstring, tdstring, totalposted); 
    6776+    if (method == 2) 
    6877+        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", 
    7079+           trstring, tdstring, tdstring, totalposted); 
    7180     printf("<tr %s><th colspan=2 %s>HTML transferred:</th>" 
    72         "<td colspan=2 %s>%ld bytes</td></tr>\n", 
     81        "<td colspan=2 %s>%" APR_INT64_T_FMT " bytes</td></tr>\n", 
    7382        trstring, tdstring, tdstring, totalbread); 
    74 @@ -1046,7 +1052,7 @@ 
     83@@ -1058,7 +1068,7 @@ 
    7584         printf("<tr %s><th colspan=2 %s>Transfer rate:</th>" 
    7685            "<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); 
    7887-        if (posting > 0) { 
    7988+        if (method > 0) { 
     
    8190                "<td colspan=2 %s>%.2f kb/s sent</td></tr>\n", 
    8291                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 @@ 
    92112     } 
    93113  
     
    96116+    if (method <= 0) { 
    97117+        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+        } 
    109128         snprintf_res = apr_snprintf(request, sizeof(_request), 
    110129             "%s %s HTTP/1.0\r\n" 
     
    119138     else { 
    120139+        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+        } 
    129147         snprintf_res = apr_snprintf(request,  sizeof(_request), 
    130148-            "POST %s HTTP/1.0\r\n" 
     
    135153             "%s" 
    136154             "\r\n", 
    137 +                       verb, 
     155+            verb, 
    138156             (isproxy) ? fullurl : path, 
    139157             keepalive ? "Connection: Keep-Alive\r\n" : "", 
    140158             cookie, auth, 
    141 @@ -1577,9 +1606,9 @@ 
     159@@ -1622,9 +1653,9 @@ 
    142160     reqlen = strlen(request); 
    143161  
     
    151169         if (!buff) { 
    152170             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 @@ 
    155172     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"); 
    159179     fprintf(stderr, "    -v verbosity    How much troubleshooting info to print\n"); 
    160180     fprintf(stderr, "    -w              Print out results in HTML tables\n"); 
     
    165185     fprintf(stderr, "    -y attributes   String to insert as tr attributes\n"); 
    166186     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 @@ 
    168228 #endif 
    169229  
    170230     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:" 
    173233 #ifdef USE_SSL 
    174234             "Z:f:" 
    175235 #endif 
    176 @@ -1984,9 +2015,12 @@ 
    177                  concurrency = atoi(optarg); 
     236@@ -2041,9 +2074,12 @@ 
     237                 windowsize = atoi(optarg); 
    178238                 break; 
    179239             case 'i': 
     
    190250             case 'g': 
    191251                 gnuplot = strdup(optarg); 
    192 @@ -2001,10 +2035,20 @@ 
     252@@ -2058,10 +2094,20 @@ 
    193253                 confidence = 0; 
    194254                 break; 
     
    214274                 else if (postdata) { 
    215275                     exit(r); 
    216  
  • thingfish/trunk/lib/thingfish/daemon.rb

    r560 r568  
    214214 
    215215        # 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 
    232238        end 
    233239         
  • thingfish/trunk/lib/thingfish/handler/default.rb

    r560 r568  
    243243    def handle_create_request( request, response ) 
    244244         
    245         if request.entity_bodies.length > 1 
    246             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 ] 
    247247            raise ThingFish::NotImplementedError, "multipart upload not implemented" 
    248248        end 
     
    251251 
    252252        # Store the primary resource 
    253         body, metadata = request.entity_bodies.to_a.flatten 
     253        body, metadata = request.bodies.to_a.flatten 
    254254        uuid = self.daemon.store_resource( body, metadata ) 
    255255     
     
    271271    def handle_update_uuid_request( request, response, uuid ) 
    272272         
     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 
    273278        # :TODO: Handle slow/big uploads by returning '202 Accepted' and spawning 
    274279        #         a handler thread? 
    275280        new_resource = ! @filestore.has_file?( uuid ) 
    276         body, metadata = request.get_body_and_metadata 
     281        body, metadata = request.bodies.to_a.flatten 
    277282        self.daemon.store_resource( body, metadata, uuid ) 
    278283         
  • thingfish/trunk/lib/thingfish/request.rb

    r560 r568  
    261261 
    262262 
    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. 
    265266    ###  
    266     ### +body+:: 
    267     ###    The uploaded (primary) body 
    268     ### +related_body+:: 
     267    ### +resource+:: 
     268    ###    The uploaded (primary) resource body 
     269    ### +related_resource+:: 
    269270    ###    The new resource body as an IO-like object 
    270271    ### +related_metadata+:: 
     
    306307 
    307308     
    308     ### Returns the entity bodies of the request along with any related metadata as 
    309     ### a Hash: 
    310     ### { 
    311     ###    <body io> => { <body metadata> }, 
    312     ###    ... 
    313     ### } 
    314     def entity_bodies 
    315         # Parse the request's body parts if they aren't already 
    316         unless @entity_bodies 
    317             if self.has_multipart_body? 
    318                 self.log.debug "Parsing multiple entity bodies." 
    319                 @entity_bodies, @form_metadata = self.parse_multipart_body 
    320             else 
    321                 self.log.debug "Parsing single entity body." 
    322                 body, metadata = self.get_body_and_metadata 
    323                  
    324                 @entity_bodies = { body => metadata } 
    325                 @form_metadata = {} 
    326             end 
    327  
    328             self.log.debug "Parsed %d bodies and %d form_metadata (%p)" %  
    329                 [@entity_bodies.length, @form_metadata.length, @form_metadata.keys] 
    330         end 
    331  
    332         return @entity_bodies 
    333     end 
    334  
    335  
    336309    ### 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+  
    338311    ### is +true+, any resources which have been appended will be yielded immediately after the 
    339312    ### body to which they are related. Note that this applies even in the current loop -- the 
     
    360333     
    361334     
     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     
    362348    ### Check the body IO objects to ensure they're still open. 
    363349    def check_body_ios 
     
    377363                @entity_bodies[ clone ] = @entity_bodies.delete( body ) if @entity_bodies.key?( body ) 
    378364                @related_resources[ clone ] = @related_resources.delete( body ) if @related_resources.key?( body ) 
     365                @metadata[ clone ] = @metadata.delete( body ) if @metadata.key?( body ) 
    379366                @related_resources.each do |_,hash| 
    380367                    hash[ clone ] = hash.delete( body ) if hash.key?( body ) 
     
    499486    ######### 
    500487 
    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 
    503518        raise ArgumentError, "Can't return a single body for a multipart request" if 
    504519            self.has_multipart_body? 
    505520         
    506         default_metadata = { 
     521        metadata = { 
    507522            :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 
    509526        } 
    510527 
     
    512529        if self.headers[:content_disposition] && 
    513530            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 
    527535    end 
    528536     
  • thingfish/trunk/plugins/thingfish-filter-mp3/lib/thingfish/filter/mp3.rb

    r497 r568  
    2424# Please see the LICENSE file for licensing details. 
    2525# 
     26 
    2627 
    2728begin 
     
    5657    NULL = "\x0" 
    5758     
     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     
    5877    # The Array of types this filter is interested in 
    5978    HANDLED_TYPES = [ ThingFish::AcceptParam.parse('audio/mpeg') ] 
     
    83102        request.each_body do |body, metadata| 
    84103            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                 
    86113                request.append_metadata_for( body, mp3_metadata ) 
    87114                self.log.debug "Extracted mp3 info: %p" % [ mp3_metadata.keys ] 
     
    132159    ######### 
    133160 
    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 ) 
    138163        mp3_metadata = { 
    139             :mp3_frequency => info.samplerate, 
    140             :mp3_bitrate   => info.bitrate, 
    141             :mp3_vbr       => info.vbr, 
    142             :mp3_title     => info.tag.title, 
    143             :mp3_artist    => info.tag.artist, 
    144             :mp3_album     => info.tag.album, 
    145             :mp3_year      => info.tag.year, 
    146             :mp3_genre     => info.tag.genre, 
    147             :mp3_tracknum  => info.tag.tracknum, 
    148             :mp3_comments  => info.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, 
    149174        } 
    150175 
    151176        # 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] 
    154180             
    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                }) 
    171190            end 
    172191        end 
     
    174193        return sanitize_values( mp3_metadata ) 
    175194    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 
    176234     
    177235     
     
    199257    end 
    200258 
    201      
    202      
    203259end # class ThingFish::Mp3Filter 
    204260 
    205261# vim: set nosta noet ts=4 sw=4: 
    206  
    207  
  • thingfish/trunk/plugins/thingfish-filter-mp3/spec/thingfish/filter/mp3_spec.rb

    r496 r568  
    5454    } 
    5555     
     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 
    5662 
    5763    before( :each ) do 
    5864        @filter = ThingFish::Filter.create( 'mp3' ) 
    5965     
    60         @io = StringIO.new( TEST_CONTENT ) 
    61         @io.stub!( :path ).and_return( :a_dummy_path ) 
    6266        @response = stub( "response object" ) 
    63  
    6467        @request_metadata = { :format => 'audio/mpeg' } 
    6568        @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 ) 
    7369    end 
    7470     
     
    7672    ### Shared behaviors 
    7773    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 
    17375    it "ignores non-POST requests" do 
    17476        @request.should_receive( :http_method ). 
     
    17981        @filter.handle_request( @request, @response ) 
    18082    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