├── __init__.py ├── test-data ├── .gitignore ├── sampleconfig.py ├── README.md ├── runflickrfs.py └── flickrfs.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-data: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | *.pyc -------------------------------------------------------------------------------- /sampleconfig.py: -------------------------------------------------------------------------------- 1 | api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 2 | api_secret = 'yyyyyyyyyyyyyyyy' 3 | user_id = 'zzzzzzzzzzzz' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flickr-FS 2 | ========= 3 | 4 | Flickr-fs is a fuse filesystem that allows you to upload your files to flickr 5 | as encoded `.png`s. It's also really slow and liable to explode at any moment, 6 | so don't seriously use it. 7 | 8 | To try this, create a folder somewhere (I suggest 9 | /[media|mnt]/[username]/flickrfs). Then copy sampleconfig.py to config.py and 10 | update the values to match ones you get from flickr's create an app page. Then, 11 | run `python2 runflickrfs.py `, where folder is the folder you created 12 | earlier. If everything goes well, after a few seconds you should get the message 13 | that it sucessfully mounted. You can then run a few simple operations like 14 | `ls`/`cp` on the filesystem. 15 | 16 | The FS is pretty slow and unoptimized, but not unusable. 17 | 18 | time /bin/ls /media/me/mntpoint 19 | test test1 20 | /bin/ls /media/me/mntpoint 0.00s user 0.00s system 0% cpu 1.830 total 21 | 22 | However, it has the tendency to stall out sometimes and crash with a 503. I'm 23 | not sure if that's because I'm doing something wrong or because Flickr is 24 | throttling me. 25 | 26 | Requirements: 27 | python 2.7.3 28 | beautifulsoup4 29 | flickrapi 30 | fs 31 | PIL 32 | requests -------------------------------------------------------------------------------- /runflickrfs.py: -------------------------------------------------------------------------------- 1 | from fs.expose import fuse 2 | from flickrfs import FlickrFS, data_to_png, png_to_data 3 | import requests 4 | import sys 5 | 6 | 7 | if __name__ == '__main__': 8 | if sys.argv[1] == 'encode': 9 | filefrom, fileto = sys.argv[2], sys.argv[3] 10 | with open(filefrom) as ff, open(fileto, 'w') as ft: 11 | img = data_to_png(ff.read()) 12 | img.save(ft, 'png') 13 | elif sys.argv[1] == 'decode': 14 | filefrom, fileto = sys.argv[2], sys.argv[3] 15 | with open(filefrom) as ff, open(fileto, 'w') as ft: 16 | data = png_to_data(ff.read()) 17 | ft.write(data) 18 | elif sys.argv[1] == 'upload': 19 | filefrom = sys.argv[2] 20 | with open(filefrom) as ff, tempfile.NamedTemporaryFile() as tf: 21 | img = data_to_png(ff.read()) 22 | img.save(tf, 'png') 23 | print flickr.upload(filename=tf.name, format='bs4').photoid.text 24 | elif sys.argv[1] == 'download': 25 | imageid, fileto = sys.argv[2], sys.argv[3] 26 | with open(fileto, 'w') as ft: 27 | url = flickr.get_sizes(photo_id=imageid, format='bs4').sizes.find('size', label='Original')['source'] 28 | ft.write(png_to_data(requests.get(url).content)) 29 | elif len(sys.argv) == 2: 30 | mp = fuse.mount(FlickrFS(), sys.argv[1]) 31 | print 'mounted your filckr account on', mp.path, 'pid', mp.pid, '.' 32 | else: 33 | print "Usage:" 34 | print "python runflickrfs.py - mount your flickr account as a FUSE filesystem" 35 | print "python runflickrfs.py encode - encode the contents of a file as a png" 36 | print "python runflickrfs.py decode - decode the .png from into it's original contents" 37 | print "python runflickrfs.py upload - upload the file to flickr and print the photo id" 38 | print "python runflickrfs.py download - download a photo and decode it to it's original contents" 39 | -------------------------------------------------------------------------------- /flickrfs.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import flickrapi 3 | import fs 4 | import fs.base 5 | import fs.errors 6 | import os 7 | from PIL import Image 8 | from StringIO import StringIO 9 | import requests 10 | import struct 11 | import sys 12 | import tempfile 13 | import traceback 14 | 15 | try: 16 | from config import api_key, api_secret, user_id 17 | except ImportError: 18 | print 'You need to create a config file!' 19 | print 'Copy sampleconfig.py to config.py and fill in the values' 20 | print 'You need to register at www.flickr.com/services/apps/create/' 21 | print 'You can find your user id in the "Useful Values" section of www.flickr.com/services/api/explore/flickr.activity.userComments' 22 | sys.exit(1) 23 | 24 | 25 | @flickrapi.rest_parser('bs4') 26 | def parse_bs4(self, rest_xml): 27 | """Register BeautifulSoup as a format""" 28 | xml = BeautifulSoup(rest_xml, features="xml") 29 | if xml.rsp['stat'] == 'ok': 30 | return xml.rsp 31 | raise flickrapi.FlickrError(u'Error: {0}: {1}'.format(xml.rsp.err['code'], xml.rsp.err['msg'])) 32 | 33 | 34 | def clump(l, n): 35 | """Clump a list into groups of length n (the last group may be shorter than n)""" 36 | return [l[i:i + n] for i in range(0, len(l), n)] 37 | 38 | 39 | def data_to_png(bytes): 40 | """Encode a byte buffer as a png, where the buffer is 8 bytes of the data length, then the data""" 41 | length = struct.pack('L', len(bytes)) 42 | clumps = clump(length + bytes, 3) 43 | # pad last list out to 3 elements 44 | clumps[-1] += "\x00\x00" 45 | clumps[-1] = clumps[-1][:3] 46 | clumps = map(lambda t: tuple(map(ord, t)), clumps) 47 | # create img 48 | img = Image.new('RGB', (len(clumps), 1)) 49 | img.putdata(map(tuple, clumps)) 50 | return img 51 | 52 | 53 | def png_to_data(imgdata): 54 | img = Image.open(StringIO(imgdata)) 55 | bytes = sum(list(img.getdata()), ()) 56 | length, data = ''.join(map(chr, bytes[:8])), bytes[8:] 57 | return ''.join(map(chr, data[:struct.unpack('L', length)[0]])) 58 | 59 | flickr = flickrapi.FlickrAPI(api_key, api_secret) 60 | flickr.token.path = '/tmp/flickrtokens' 61 | 62 | (token, frob) = flickr.get_token_part_one(perms='delete') 63 | if not token: 64 | raw_input("Press ENTER after you authorized this program") 65 | flickr.get_token_part_two((token, frob)) 66 | 67 | # Register some nicer names for API endpoints 68 | flickr.delete = flickr.photos_delete 69 | flickr.get_sizes = flickr.photos_getSizes 70 | flickr.set_meta = flickr.photos_setMeta 71 | flickr.get_meta = flickr.photos_getInfo 72 | 73 | 74 | class FlickrFile(object): 75 | 76 | """A file-like object representing a file on flickr. Caches with a StringIO object""" 77 | 78 | def __init__(self, imageid, name, data): 79 | self.imageid = imageid 80 | self.name = name 81 | self.stringio = StringIO(data) 82 | self.closed = False 83 | self.newlines = ('\r', '\n', '\r\n') 84 | self.flush() 85 | 86 | def close(self): 87 | self.flush() 88 | self.closed = True 89 | 90 | def _stringio_get_data(self): 91 | old_seek = self.stringio.tell() 92 | self.stringio.seek(0) 93 | data = self.stringio.read() 94 | self.stringio.seek(old_seek) 95 | return data 96 | 97 | def flush(self): 98 | with tempfile.NamedTemporaryFile() as tf: 99 | data_to_png(self._stringio_get_data()).save(tf, 'png') 100 | if self.imageid: 101 | flickr.replace(filename=tf.name, photo_id=self.imageid, title=self.name, description=str(len(data)), format='bs4') 102 | else: 103 | self.imageid = flickr.upload(filename=tf.name, title=self.name, description=str(len(data)), format='bs4').photoid.text 104 | 105 | def iter(self): 106 | return self 107 | 108 | def next(self): 109 | return self.stringio.next() 110 | 111 | def read(self, size=-1): 112 | return self.stringio.read(size) 113 | 114 | def readline(self, size=-1): 115 | return self.stringio.read(size) 116 | 117 | def seek(self, offset, whence=0): 118 | return self.stringio.seek(offset, whence) 119 | 120 | def tell(self): 121 | return self.stringio.tell() 122 | 123 | def truncate(self, size=0): 124 | return self.stringio.truncate(size) 125 | 126 | def write(self, data): 127 | return self.stringio.write(data) 128 | 129 | def writelines(self, seq): 130 | return self.stringio.writelines(seq) 131 | 132 | 133 | class FlickrFS(fs.base.FS): 134 | 135 | """A PyFilesystem object representing your flickr account""" 136 | 137 | def __init__(self): 138 | super(FlickrFS, self).__init__() 139 | self._flickr_name_cache = {} 140 | 141 | def _norm_path(self, path): 142 | if path.startswith('/'): 143 | path = path[1:] 144 | if path.endswith('/'): 145 | path = path[:-1] 146 | return path 147 | 148 | def _lookup_flickr_title(self, title): 149 | title = self._norm_path(title) 150 | if title in self._flickr_name_cache: 151 | fid = self._flickr_name_cache[title] 152 | try: 153 | flickr_title = flickr.get_meta(photo_id=fid, format='bs4').title.text 154 | if flickr_title == title: 155 | return fid 156 | except: 157 | pass 158 | for photo in flickr.walk(user_id=user_id): 159 | if photo.get('title') == title: 160 | self._flickr_name_cache[title] = photo.get('id') 161 | return photo.get('id') 162 | return None 163 | 164 | def open(self, path, mode='r'): # ignore mode because wdgaf 165 | path = self._norm_path(path) 166 | fid = self._lookup_flickr_title(path) 167 | data = '' 168 | if fid: 169 | url = flickr.get_sizes(photo_id=fid, format='bs4').sizes.find('size', label='Original')['source'] 170 | data = png_to_data(requests.get(url).content) 171 | return FlickrFile(fid, path, data) 172 | 173 | def exists(self, path): 174 | if path in ('', '/'): 175 | return True 176 | return self._lookup_flickr_title(path) is not None 177 | 178 | def isfile(self, path): 179 | path = self._norm_path(path) 180 | return path not in ('', '/') and exists(path) 181 | 182 | def isdir(self, path): 183 | return path in ('', '/') 184 | 185 | def listdir(self, path='/', wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): 186 | if path not in ('', '/'): 187 | raise fs.errors.ResourceNotFoundError(path) 188 | paths = [unicode(self._norm_path(photo.get('title'))) for photo in flickr.walk(user_id=user_id)] 189 | return self._listdir_helper(path, paths, wildcard, full, absolute, dirs_only, files_only) 190 | 191 | def makedir(self, path): 192 | raise ValueError('FlickrFS does not support creating directories') 193 | 194 | def remove(self, path): 195 | if path in ('', '/'): 196 | raise fs.errors.ResourceInvalidError(path) 197 | path = self._norm_path(path) 198 | if path in self._flickr_name_cache: 199 | del self._flickr_name_cache[path] 200 | fid = self._lookup_flickr_title(path) 201 | if not fid: 202 | raise fs.errors.ResourceNotFoundError(path) 203 | flickr.delete(photo_id=fid, format='bs4') 204 | 205 | def removedir(self, path, recursive=False, force=False): 206 | raise ValueError('FlickrFS does not support creating directories, so why are you removing them?') 207 | 208 | def rename(self, src, dst): 209 | if src in ('', '/') or dst in ('', '/'): 210 | raise ResourceInvalidError('Can\'t rename root') 211 | src = self._norm_path(src) 212 | dst = self._norm_path(dst) 213 | fid = self._lookup_flickr_title(src) 214 | if not fid: 215 | raise ResourceNotFoundError(src) 216 | flickr.set_meta(photo_id=fid, title=dst, format='bs4') 217 | 218 | def getinfo(self, path): 219 | if path in ('', '/'): 220 | return {'size': 0} 221 | path = self._norm_path(path) 222 | fid = self._lookup_flickr_title(path) 223 | if fid: 224 | url = flickr.get_sizes(photo_id=fid, format='bs4').sizes.find('size', label='Original')['source'] 225 | data = png_to_data(requests.get(url).content) 226 | return {'size': len(data)} 227 | else: 228 | raise fs.errors.ResourceNotFoundError(path) 229 | --------------------------------------------------------------------------------