Google App EngineによるTumblr画像のバックアップ

 Tumblr楽しいですよね。自分好みの文章やら画像をどんどんリブログする。でも、いつサービスが停止するかわからない。毎日貯めたお気に入り画像が消えちゃうのは嫌だ。ここはやっぱりバックアップしとかなきゃ、となるわけです。それで以前はtomblooFlickrに同時ポストとかしてたけど、これもログインがめんどくて、そもそも過去ポストのバックアップができない。いろいろ考えた結果、メール(自分の場合はGmail)をストレージ代わりにしてTumblr画像を添付でどんどん送りつけることにした。プラットフォームはもちろんGoogle App Engine。流れとして、まずTumblrの画像のURLをすべて取得する。その後、CronとTaskqueueを使って画像取得とメール送信を行う。
http://gist.github.com/316633

class TumblrPhoto(db.Model):
    url = db.StringProperty()
    queued = db.BooleanProperty()
    mailsent = db.BooleanProperty()
    content = db.BlobProperty()

class StartHandler(webapp.RequestHandler):
    def get(self):
        url = 'http://' + tumblr_account + '.tumblr.com/api/read?type=photo'
        result = urlfetch.fetch(url)
        if result.status_code == 200:
            soup = BeautifulStoneSoup(result.content)
            posts = soup('posts')
            end = posts[0]['total']
            taskqueue.add(url='/tumblr/check/0/'+end, method='GET')
            self.response.out.write('Task start.')
        else:
            self.response.out.write('error')

まずバックアップタスクを開始させるハンドラ。ここではTumblrの画像ポストの総数を求めて、それをパラメータとしてTaskqueueで画像URL取得を開始させるだけ。

class CheckHandler(webapp.RequestHandler):
    def get(self, start='', end=''):
        if not start or not end:
            return
        
        url = 'http://' + tumblr_account + '.tumblr.com/api/read?num=50&type=photo&start=' + start
        result = urlfetch.fetch(url)
        if result.status_code == 200:
            soup = BeautifulStoneSoup(result.content)
            for (post, photo_url) in zip(soup('post'), soup('photo-url', {'max-width': '1280'})):
                self.response.out.write(post['id']+','+photo_url.string+'<br>')
                
                p = TumblrPhoto.get_by_key_name(post['id'])
                if p is None:
                    p = TumblrPhoto(key_name=post['id'], url=photo_url.string+'', queued=False, mailsent=False)
                    
                    p.put()
            
            if int(start)+50 < int(end):
                start = str(int(start)+50)
                taskqueue.add(url='/tumblr/check/'+start+'/'+end, method='GET')
        else:
            taskqueue.add(url='/tumblr/check/'+start+'/'+end, method='GET')

ここではパラメータで与えられた開始位置から50個の画像ポストのXMLを取得。そのXMLをBeautifulStoneSoupでパースしてポストIDといちばんサイズの大きな画像のURLを取り出してTumblrPhotoに保存。パラメータの画像ポスト総数と比較して、まだ画像ポストが残っているようだったら、取得開始位置を50ずらして再びTaskqueue追加。

class CronHandler(webapp.RequestHandler):
    def get(self):
        photos = db.Query(TumblrPhoto).filter('queued =', False).fetch(10)
        for photo in photos:
            taskqueue.add(url='/tumblr/fetch/'+photo.key().name(), method='GET')
            photo.queued = True
            photo.put()

これはCronで実行する。Taskqueueで実行してもよいのだけど、それだとリソースの減りが早く、万が一リソースをすべて使い切ってエラー多発もあり得るので、ここはCronで時間をかけて画像を取得する。このハンドラは単純にTumblrPhotoから画像未取得のエンティティを10件取ってきて、それぞれに対して画像取得ハンドラをTaskqueueで追加。

class FetchHandler(webapp.RequestHandler):
    def get(self, id=''):
        if not id:
            return
        
        photo = TumblrPhoto.get_by_key_name(id)
        if photo is None:
            return
        
        result = urlfetch.fetch(photo.url)
        if result.status_code != 200:
            taskqueue.add(url='/tumblr/fetch/'+id, method='GET')
            return
        
        photo.content = result.content
        photo.put()
        taskqueue.add(url='/tumblr/mail/'+id, method='GET')

画像取得ハンドラ。取得に成功したら画像をTumblrPhotoに保存し、メール送信ハンドラをTaskqueueに追加。

class MailHandler(webapp.RequestHandler):
    def get(self, id=''):
        if not id:
            return
        
        photo = TumblrPhoto.get_by_key_name(id)
        if photo is None:
            return
        
        filename = photo.url
        if not re.compile('^.*\.jpe?g$').search(filename):
            alphabets = string.digits + string.letters
            filename = ''.join(random.choice(alphabets) for i in xrange(20)) + '.jpg'
        
        mail.send_mail(
            sender = from_mailaddress,
            to = to_mailaddress,
            subject = filename,
            body = filename,
            attachments = [(filename, photo.content)]
        )
        
        photo.content = None
        photo.mailsent = True
        photo.put()

最後のメール送信ハンドラ。ファイル名処理をして、画像を添付してメール送信。メール送信したらTumblrPhotoの画像を消去。
 最初の段階では画像取得とメール送信をひとつのハンドラで処理してたんだけど、メール送信のエラーが多くて、再実行の度に毎回Tumblrから画像を取ってくるのは非効率な気がしたので、画像取得部分とメール送信部分を分割した。なんでメール送信のエラーが多いか調べたら、原因はGAEのリソース制限。無料のGAEだと、1日に送信できる添付ファイルの総量は100MBで、かつ最大レートが設定されていて添付ファイル付きメール送信が1分あたり8通、しかも1分あたり560KBの添付ファイルまでと、かなり厳しい。ということは560KBより大きい添付ファイルは送れない。サイズの大きい画像の処置をどうにかしないといけない。そして、Cron使用を前提で計算すると1添付ファイル400KBとして1時間に10通ぐらいが限度。自分の今のTumblrは約3000ポストだから、1日240通として、えーと、約13日(泣)。使えない。いや、バックアップだから速さは特に必要ないわけで使えなくはない。でもなあ、リソース制限厳しい。
http://code.google.com/intl/ja/appengine/docs/quotas.html#Mail
また、バックアップ後に使いやすくするためにはファイル名をどうするか、タグをどうするかとか考えないといけない。どんなかたちでもいいからとにかくバックアップしたい人向け。