Line 0
Link Here
|
|
|
1 |
#!/usr/bin/python |
2 |
#=============================================================================== |
3 |
# flickrfs - Virtual Filesystem for Flickr |
4 |
# Copyright (c) 2005,2006 Manish Rai Jain <manishrjain@gmail.com> |
5 |
# |
6 |
# This program can be distributed under the terms of the GNU GPL version 2, or |
7 |
# its later versions. |
8 |
# |
9 |
# DISCLAIMER: The API Key and Shared Secret are provided by the author in |
10 |
# the hope that it will prevent unnecessary trouble to the end-user. The |
11 |
# author will not be liable for any misuse of this API Key/Shared Secret |
12 |
# through this application/derived apps/any 3rd party apps using this key. |
13 |
#=============================================================================== |
14 |
|
15 |
__author__ = "Manish Rai Jain (manishrjain@gmail.com)" |
16 |
__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" |
17 |
|
18 |
import thread, string, ConfigParser, mimetypes, codecs |
19 |
import time, logging, logging.handlers, os, sys |
20 |
from glob import glob |
21 |
from errno import * |
22 |
from traceback import format_exc |
23 |
# The python-fuse api has changed in 0.2, but we're still using the 0.1 api. |
24 |
# The following two lines line will make us compatible with both versions by |
25 |
# making 2.0 use the 1.0 api. |
26 |
# (See http://fuse4bsd.creo.hu/README.new_fusepy_api.html) |
27 |
import fuse |
28 |
fuse.fuse_python_api = (0, 1) |
29 |
Fuse = fuse.Fuse |
30 |
import threading |
31 |
import random, commands |
32 |
from urllib2 import URLError |
33 |
from transactions import TransFlickr |
34 |
import inodes |
35 |
|
36 |
#Some global definitions and functions |
37 |
NUMRETRIES = 3 |
38 |
DEFAULTCONFIG = """\ |
39 |
[configuration] |
40 |
|
41 |
browser: /usr/bin/x-www-browser |
42 |
image.size: |
43 |
sets.sync.int: 300 |
44 |
stream.sync.int: 300 |
45 |
""" |
46 |
|
47 |
#Set up the .flickfs directory. |
48 |
homedir = os.getenv('HOME') |
49 |
flickrfsHome = os.path.join(homedir, '.flickrfs') |
50 |
dbPath = os.path.join(flickrfsHome, '.inode.bdb') |
51 |
|
52 |
if not os.path.exists(flickrfsHome): |
53 |
os.mkdir(os.path.join(flickrfsHome)) |
54 |
else: |
55 |
# Remove previous metadata files from ~/.flickrfs |
56 |
for a in glob(os.path.join(flickrfsHome, '.*')): |
57 |
os.remove(os.path.join(flickrfsHome, a)) |
58 |
try: |
59 |
os.remove(dbPath) |
60 |
except: |
61 |
pass |
62 |
|
63 |
# Added by Varun Hiremath |
64 |
if not os.path.exists(flickrfsHome + "/config.txt"): |
65 |
fconfig = open(flickrfsHome+"/config.txt",'w') |
66 |
fconfig.write(DEFAULTCONFIG) |
67 |
fconfig.close() |
68 |
|
69 |
# Set up logging |
70 |
rootlogger = logging.getLogger() |
71 |
loghdlr = logging.handlers.RotatingFileHandler( |
72 |
os.path.join(flickrfsHome,'log'), "a", 5242880, 3) |
73 |
logfmt = logging.Formatter("%(asctime)s %(name)-14s %(levelname)-7s %(threadName)-10s %(funcName)-22s %(message)s", "%x %X") |
74 |
loghdlr.setFormatter(logfmt) |
75 |
rootlogger.addHandler(loghdlr) |
76 |
rootlogger.setLevel(logging.DEBUG) |
77 |
log = logging.getLogger('flickrfs') |
78 |
logattr = logging.getLogger('flickrfs.attr') |
79 |
logattr.setLevel(logging.INFO) |
80 |
|
81 |
cp = ConfigParser.ConfigParser() |
82 |
cp.read(flickrfsHome + '/config.txt') |
83 |
_resizeStr = "" |
84 |
sets_sync_int = 600.0 |
85 |
stream_sync_int = 600.0 |
86 |
try: |
87 |
_resizeStr = cp.get('configuration', 'image.size') |
88 |
except: |
89 |
print 'No default size of image found. Will upload original size of images.' |
90 |
try: |
91 |
sets_sync_int = float(cp.get('configuration', 'sets.sync.int')) |
92 |
except: |
93 |
pass |
94 |
try: |
95 |
stream_sync_int = float(cp.get('configuration', 'stream.sync.int')) |
96 |
except: |
97 |
pass |
98 |
try: |
99 |
browserName = cp.get('configuration', 'browser') |
100 |
except: |
101 |
pass |
102 |
|
103 |
# Retrive the resize string. |
104 |
def GetResizeStr(): |
105 |
return _resizeStr |
106 |
|
107 |
#Utility functions. |
108 |
def _log_exception_wrapper(func, *args, **kw): |
109 |
"""Call 'func' with args and kws and log any exception it throws. |
110 |
""" |
111 |
for i in range(0, NUMRETRIES): |
112 |
log.debug("retry attempt %s for func %s", i, func.__name__) |
113 |
try: |
114 |
func(*args, **kw) |
115 |
return |
116 |
except: |
117 |
log.error("exception in function %s", func.__name__) |
118 |
log.error(format_exc()) |
119 |
|
120 |
def background(func, *args, **kw): |
121 |
"""Run 'func' as a thread, logging any exceptions it throws. |
122 |
|
123 |
To run |
124 |
|
125 |
somefunc(arg1, arg2='value') |
126 |
|
127 |
as a thread, do: |
128 |
|
129 |
background(somefunc, arg1, arg2='value') |
130 |
|
131 |
Any exceptions thrown are logged as errors, and the traceback is logged. |
132 |
""" |
133 |
thread.start_new_thread(_log_exception_wrapper, (func,)+args, kw) |
134 |
|
135 |
def timerThread(func, func1, interval): |
136 |
'''Execute func now, followed by func1 every interval seconds |
137 |
''' |
138 |
log.debug("running first pass funtion %s", func.__name__) |
139 |
t = threading.Timer(0.0, func) |
140 |
try: |
141 |
t.run() |
142 |
except: |
143 |
log.debug(format_exc()) |
144 |
while(interval): |
145 |
log.debug("scheduling function %s to run in %s seconds", |
146 |
func1.__name__, interval) |
147 |
t = threading.Timer(interval, func1) |
148 |
try: |
149 |
t.run() |
150 |
except: |
151 |
log.debug(format_exc()) |
152 |
|
153 |
def retryFlickrOp(isNone, func, *args): |
154 |
# This function helps in retrying the flickr transactions, in case they fail. |
155 |
result = None |
156 |
for i in range(0, NUMRETRIES): |
157 |
log.debug("retry attempt %d for func %s", i, func.__name__) |
158 |
try: |
159 |
result = func(*args) |
160 |
if result is None: |
161 |
if isNone: |
162 |
return result |
163 |
else: |
164 |
continue |
165 |
else: |
166 |
return result |
167 |
except URLError, detail: |
168 |
log.error("URLError in function %s with error: %s", |
169 |
func.__name__, detail) |
170 |
# We've utilized all our attempts, send out the result whatever it is. |
171 |
return result |
172 |
|
173 |
class Flickrfs(Fuse): |
174 |
|
175 |
def __init__(self, *args, **kw): |
176 |
|
177 |
Fuse.__init__(self, *args, **kw) |
178 |
log.info("mountpoint: %s", repr(self.mountpoint)) |
179 |
log.info("mount options: %s", ', '.join(self.optlist)) |
180 |
log.info("named mount options: %s", |
181 |
', '.join([ "%s: %s" % (k, v) for k, v in self.optdict.items() ])) |
182 |
|
183 |
self.inodeCache = inodes.InodeCache(dbPath) # Inodes need to be stored. |
184 |
self.imgCache = inodes.ImageCache() |
185 |
self.NSID = "" |
186 |
self.transfl = TransFlickr(browserName) |
187 |
|
188 |
# Set some variables to be utilized by statfs function. |
189 |
self.statfsCounter = -1 |
190 |
self.max = 0L |
191 |
self.used = 0L |
192 |
|
193 |
self.NSID = self.transfl.getUserId() |
194 |
if self.NSID is None: |
195 |
log.error("can't retrieve user information") |
196 |
sys.exit(-1) |
197 |
|
198 |
log.info('getting list of licenses available') |
199 |
self.licenses = self.transfl.getLicenses() |
200 |
if self.licenses is None: |
201 |
log.error("can't retrieve license information") |
202 |
sys.exit(-1) |
203 |
|
204 |
# do stuff to set up your filesystem here, if you want |
205 |
self._mkdir("/") |
206 |
self._mkdir("/tags") |
207 |
self._mkdir("/tags/personal") |
208 |
self._mkdir("/tags/public") |
209 |
background(timerThread, self.sets_thread, |
210 |
self.sync_sets_thread, sets_sync_int) #sync every 2 minutes |
211 |
|
212 |
|
213 |
def imageResize(self, bufData): |
214 |
# If no resizing information is present, then return the buffer directly. |
215 |
if GetResizeStr() == "": |
216 |
return bufData |
217 |
|
218 |
# Else go ahead and do the conversion. |
219 |
im = '/tmp/flickrfs-' + str(int(random.random()*1000000000)) |
220 |
f = open(im, 'w') |
221 |
f.write(bufData) |
222 |
f.close() |
223 |
cmd = 'identify -format "%%w" %s'%(im,) |
224 |
status,ret = commands.getstatusoutput(cmd) |
225 |
msg = ("%s command not found; you must install Imagemagick to get " |
226 |
"auto photo resizing") |
227 |
if status!=0: |
228 |
print msg % 'identify' |
229 |
log.error(msg, identify) |
230 |
return bufData |
231 |
try: |
232 |
if int(ret)<int(GetResizeStr().split('x')[0]): |
233 |
log.info('image size is smaller than size specified in config.txt;' |
234 |
' retaining original size') |
235 |
return bufData |
236 |
except: |
237 |
log.error('invalid format of image.size in config.txt') |
238 |
return bufData |
239 |
log.debug("resizing image %s to size %s" % (im, GetResizeStr())) |
240 |
cmd = 'convert %s -resize %s %s-conv'%(im, GetResizeStr(), im) |
241 |
ret = os.system(cmd) |
242 |
if ret!=0: |
243 |
print msg % 'convert' |
244 |
log.error(msg, 'convert') |
245 |
return bufData |
246 |
else: |
247 |
f = open(im + '-conv') |
248 |
return f.read() |
249 |
|
250 |
|
251 |
def writeMetaInfo(self, id, INFO): |
252 |
#The metadata may be unicode strings, so we need to encode them on write |
253 |
filePath = os.path.join(flickrfsHome, '.'+id) |
254 |
f = codecs.open(filePath, 'w', 'utf8') |
255 |
f.write('# Metadata file : flickrfs - Virtual filesystem for flickr\n') |
256 |
f.write('# Photo owner: %s NSID: %s\n' % (INFO[7], INFO[8])) |
257 |
f.write('# Handy link to photo: %s\n'%(INFO[9])) |
258 |
f.write('# Licences available: \n') |
259 |
for (k, v) in self.licenses: |
260 |
f.write('# %s : %s\n' % (k, v)) |
261 |
f.write('[metainfo]\n') |
262 |
f.write("%s:%s\n"%('title', INFO[4])) |
263 |
f.write("%s:%s\n"%('description', INFO[3])) |
264 |
tags = ','.join(INFO[5]) |
265 |
f.write("%s:%s\n"%('tags', tags)) |
266 |
f.write("%s:%s\n"%('license',INFO[6])) |
267 |
f.close() |
268 |
f = open(filePath) |
269 |
f.read() |
270 |
fileSize = f.tell() |
271 |
f.close() |
272 |
return fileSize |
273 |
|
274 |
def __populate_set(self, set_id, curdir): |
275 |
# Exception handling will be done by background function. |
276 |
photosInSet = self.transfl.getPhotosFromPhotoset(set_id) |
277 |
for b,p in photosInSet.iteritems(): |
278 |
info = self.transfl.parseInfoFromPhoto(b,p) |
279 |
self._mkfileWithMeta(curdir, info) |
280 |
log.info("set %s populated, photo count %s", curdir, len(photosInSet)) |
281 |
|
282 |
def sets_thread(self): |
283 |
""" |
284 |
The beauty of the FUSE python implementation is that with the |
285 |
python interpreter running in foreground, you can have threads |
286 |
""" |
287 |
print "Sets are being populated in the background." |
288 |
log.info("started") |
289 |
self._mkdir("/sets") |
290 |
for a in self.transfl.getPhotosetList(): |
291 |
title = a.title[0].elementText.replace('/', '_') |
292 |
log.info("populating set %s", title) |
293 |
curdir = "/sets/" + title |
294 |
if title.strip()=='': |
295 |
curdir = "/sets/" + a['id'] |
296 |
set_id = a['id'] |
297 |
self._mkdir(curdir, id=set_id) |
298 |
self.__populate_set(set_id, curdir) |
299 |
|
300 |
def _sync_code(self, psetOnline, curdir): |
301 |
psetLocal = set(map(lambda x: x[0], self.getdir(curdir, False))) |
302 |
for b in psetOnline: |
303 |
info = self.transfl.parseInfoFromPhoto(b) |
304 |
imageTitle = info.get('title','') |
305 |
if hasattr(b, 'originalformat'): |
306 |
imageTitle = self.__getImageTitle(imageTitle, |
307 |
b['id'], b['originalformat']) |
308 |
else: |
309 |
imageTitle = self.__getImageTitle(imageTitle, b['id']) |
310 |
path = "%s/%s"%(curdir, imageTitle) |
311 |
inode = self.inodeCache.get(path) |
312 |
# This exception throwing is just for debugging. |
313 |
if inode == None and self.inodeCache.has_key(path): |
314 |
e = OSError("Path %s present in inodeCache" % path) |
315 |
e.errno = ENOENT |
316 |
raise e |
317 |
if inode == None: # Image inode not present in the set. |
318 |
log.debug("new image found: %s", path) |
319 |
self._mkfileWithMeta(curdir, info) |
320 |
else: |
321 |
if inode.mtime != int(info.get('dupdate')): |
322 |
log.debug("image %s changed", path) |
323 |
self.inodeCache.pop(path) |
324 |
if self.inodeCache.has_key(path + ".meta"): |
325 |
self.inodeCache.pop(path + ".meta") |
326 |
self._mkfileWithMeta(curdir, info) |
327 |
psetLocal.discard(imageTitle) |
328 |
if len(psetLocal)>0: |
329 |
log.info('%s photos have been deleted online' % len(psetLocal)) |
330 |
for c in psetLocal: |
331 |
log.info('deleting %s', c) |
332 |
self.unlink("%s/%s" % (curdir, c), False) |
333 |
|
334 |
def __sync_set_in_background(self, set_id, curdir): |
335 |
# Exception handling will be done by background function. |
336 |
log.info("syncing set %s", curdir) |
337 |
psetOnline = self.transfl.getPhotosFromPhotoset(set_id) |
338 |
self._sync_code(psetOnline, curdir) |
339 |
log.info("set %s sync successfully finished", curdir) |
340 |
|
341 |
def sync_sets_thread(self): |
342 |
log.info("started") |
343 |
setListOnline = self.transfl.getPhotosetList() |
344 |
setListLocal = self.getdir('/sets', False) |
345 |
|
346 |
for a in setListOnline: |
347 |
title = a.title[0].elementText.replace('/', '_') |
348 |
if title.strip()=="": |
349 |
title = a['id'] |
350 |
if (title,0) not in setListLocal: #New set added online |
351 |
log.info("new set %s found online", title) |
352 |
self._mkdir('/sets/'+title, a['id']) |
353 |
else: #Present Online |
354 |
setListLocal.remove((title,0)) |
355 |
for a in setListLocal: #List of sets present locally, but not online |
356 |
log.info('set %s no longer online, recursively deleting it', a) |
357 |
self.rmdir('/sets/'+a[0], online=False, recr=True) |
358 |
|
359 |
for a in setListOnline: |
360 |
title = a.title[0].elementText.replace('/', '_') |
361 |
curdir = "/sets/" + title |
362 |
if title.strip()=='': |
363 |
curdir = "/sets/" + a['id'] |
364 |
set_id = a['id'] |
365 |
self.__sync_set_in_background(set_id, curdir) |
366 |
log.info('finished') |
367 |
|
368 |
def sync_stream_thread(self): |
369 |
log.info('started') |
370 |
psetOnline = self.transfl.getPhotoStream(self.NSID) |
371 |
self._sync_code(psetOnline, '/stream') |
372 |
log.info('finished') |
373 |
|
374 |
def stream_thread(self): |
375 |
log.info("started") |
376 |
print "Populating photostream" |
377 |
for b in self.transfl.getPhotoStream(self.NSID): |
378 |
info = self.transfl.parseInfoFromPhoto(b) |
379 |
self._mkfileWithMeta('/stream', info) |
380 |
log.info("finished") |
381 |
print "Photostream population finished." |
382 |
|
383 |
def tags_thread(self, path): |
384 |
ind = string.rindex(path, '/') |
385 |
tagName = path[ind+1:] |
386 |
if tagName.strip()=='': |
387 |
log.error("the tagName '%s' doesn't contain any tags", tagName) |
388 |
return |
389 |
log.info("started for %s", tagName) |
390 |
sendtagList = ','.join(tagName.split(':')) |
391 |
if(path.startswith('/tags/personal')): |
392 |
user_id = self.NSID |
393 |
else: |
394 |
user_id = None |
395 |
for b in self.transfl.getTaggedPhotos(sendtagList, user_id): |
396 |
info = self.transfl.parseInfoFromPhoto(b) |
397 |
self._mkfileWithMeta(path, info) |
398 |
|
399 |
def getUnixPerms(self, info): |
400 |
mode = info.get('mode') |
401 |
if mode is not None: |
402 |
return mode |
403 |
perms = info.get('perms') |
404 |
if perms is None: |
405 |
return 0644 |
406 |
if perms is "1": # public |
407 |
return 0755 |
408 |
elif perms is "2": # friends only. Add 1 to 4 in middle letter. |
409 |
return 0754 |
410 |
elif perms is "3": # family only. Add 2 to 4 in middle letter. |
411 |
return 0764 |
412 |
elif perms is "4": # friends and family. Add 1+2 to 4 in middle letter. |
413 |
return 0774 |
414 |
else: |
415 |
return 0744 # private |
416 |
|
417 |
def __getImageTitle(self, title, id, format = "jpg"): |
418 |
temp = title.replace('/', '') |
419 |
# return "%s_%s.%s" % (temp[:32], id, format) |
420 |
# Store the photos original name. Thus, when pictures are uploaded |
421 |
# their names would remain as it is, allowing easy resumption of |
422 |
# uploading of images, in case some of the photos fail uploading. |
423 |
return "%s.%s" % (temp, format) |
424 |
|
425 |
def _mkfileWithMeta(self, path, info): |
426 |
# Don't write the meta information here, because it requires access to |
427 |
# the full INFO. Only do with the smaller version of information that |
428 |
# is provided. |
429 |
if info is None: |
430 |
return |
431 |
title = info.get("title", "") |
432 |
id = info.get("id", "") |
433 |
ext = info.get("format", "jpg") |
434 |
title = self.__getImageTitle(title, id, ext) |
435 |
|
436 |
# Refactor this section of code, so that it can be called |
437 |
# from read. |
438 |
# Figure out a way to retrieve information, which can be |
439 |
# used in _mkfile. |
440 |
mtime = info.get("dupdate") |
441 |
ctime = info.get("dupload") |
442 |
perms = self.getUnixPerms(info) |
443 |
self._mkfile(path+"/"+title, id=id, mode=perms, mtime=mtime, ctime=ctime) |
444 |
self._mkfile(path+'/.'+title+'.meta', id) |
445 |
|
446 |
def _parsepathid(self, path, id=""): |
447 |
#Path and Id may be unicode strings, so encode them to utf8 now before |
448 |
#we use them, otherwise python will throw errors when we combine them |
449 |
#with regular strings. |
450 |
path = path.encode('utf8') |
451 |
if id!=0: id = id.encode('utf8') |
452 |
parentDir, name = os.path.split(path) |
453 |
if parentDir=='': |
454 |
parentDir = '/' |
455 |
log.debug("parentDir %s", parentDir) |
456 |
return path, id, parentDir, name |
457 |
|
458 |
def _mkdir(self, path, id="", mtime=None, ctime=None): |
459 |
path, id, parentDir, name = self._parsepathid(path, id) |
460 |
log.debug("creating directory %s", path) |
461 |
self.inodeCache[path] = inodes.DirInode(path, id, mtime=mtime, ctime=ctime) |
462 |
if path!='/': |
463 |
pinode = self.getInode(parentDir) |
464 |
pinode.nlink += 1 |
465 |
self.updateInode(parentDir, pinode) |
466 |
log.debug("nlink of %s is now %s", parentDir, pinode.nlink) |
467 |
|
468 |
def _mkfile(self, path, id="", mode=None, |
469 |
comm_meta="", mtime=None, ctime=None): |
470 |
path, id, parentDir, name = self._parsepathid(path, id) |
471 |
log.debug("creating file %s with id %s", path, id) |
472 |
image_name, extension = os.path.splitext(name) |
473 |
if not extension: |
474 |
log.error("can't create file without extension") |
475 |
return |
476 |
fInode = inodes.FileInode(path, id, mode=mode, comm_meta=comm_meta, |
477 |
mtime=mtime, ctime=ctime) |
478 |
self.inodeCache[path] = fInode |
479 |
# Now create the meta info inode if the meta info file exists |
480 |
# refactoring: create the meta info inode, regardless of the |
481 |
# existence of datapath. |
482 |
# path = os.path.join(parentDir, '.' + image_name + '.meta') |
483 |
# datapath = os.path.join(flickrfsHome, '.'+id) |
484 |
# if os.path.exists(datapath): |
485 |
# size = os.path.getsize(datapath) |
486 |
# self.inodeCache[path] = FileInode(path, id) |
487 |
|
488 |
def getattr(self, path): |
489 |
# getattr is being called 4-6 times every second for '/' |
490 |
# Don't log those calls, as they clutter up the log file. |
491 |
if path != "/": |
492 |
logattr.debug("getattr: %s", path) |
493 |
templist = path.split('/') |
494 |
if path.startswith('/sets/'): |
495 |
templist[2] = templist[2].split(':')[0] |
496 |
elif path.startswith('/stream'): |
497 |
templist[1] = templist[1].split(':')[0] |
498 |
path = '/'.join(templist) |
499 |
|
500 |
inode=self.getInode(path) |
501 |
if inode: |
502 |
#log.debug("inode %s", inode) |
503 |
statTuple = (inode.mode,inode.ino,inode.dev,inode.nlink, |
504 |
inode.uid,inode.gid,inode.size,inode.atime,inode.mtime,inode.ctime) |
505 |
#log.debug("statsTuple %s", statTuple) |
506 |
return statTuple |
507 |
else: |
508 |
e = OSError("No such file"+path) |
509 |
e.errno = ENOENT |
510 |
raise e |
511 |
|
512 |
def readlink(self, path): |
513 |
log.debug("readlink") |
514 |
return os.readlink(path) |
515 |
|
516 |
def getdir(self, path, hidden=True): |
517 |
logattr.debug("getdir: %s", path) |
518 |
templist = [] |
519 |
if hidden: |
520 |
templist = ['.', '..'] |
521 |
for a in self.inodeCache.keys(): |
522 |
ind = a.rindex('/') |
523 |
if path=='/': |
524 |
path="" |
525 |
if path==a[:ind]: |
526 |
name = a.split('/')[-1] |
527 |
if name=="": |
528 |
continue |
529 |
if hidden and name.startswith('.'): |
530 |
templist.append(name) |
531 |
elif not name.startswith('.'): |
532 |
templist.append(name) |
533 |
return map(lambda x: (x,0), templist) |
534 |
|
535 |
def unlink(self, path, online=True): |
536 |
log.debug("unlink %s", path) |
537 |
if self.inodeCache.has_key(path): |
538 |
inode = self.inodeCache.pop(path) |
539 |
# Remove the meta data file as well if it exists |
540 |
if self.inodeCache.has_key(path + ".meta"): |
541 |
self.inodeCache.pop(path + ".meta") |
542 |
|
543 |
typesinfo = mimetypes.guess_type(path) |
544 |
if typesinfo[0] is None or typesinfo[0].count('image')<=0: |
545 |
log.debug("unlinked non-image file %s", path) |
546 |
return |
547 |
|
548 |
if path.startswith('/sets/'): |
549 |
ind = path.rindex('/') |
550 |
pPath = path[:ind] |
551 |
pinode = self.getInode(pPath) |
552 |
if online: |
553 |
self.transfl.removePhotofromSet(photoId=inode.photoId, |
554 |
photosetId=pinode.setId) |
555 |
log.info("photo %s removed from set", path) |
556 |
del inode |
557 |
else: |
558 |
log.error("%s is not a known file", path) |
559 |
#Dont' raise an exception. Not useful when |
560 |
#using editors like Vim. They make loads of |
561 |
#crap buffer files |
562 |
|
563 |
def rmdir(self, path, online=True, recr=False): |
564 |
log.debug("removing %s", path) |
565 |
if self.inodeCache.has_key(path): |
566 |
for a in self.inodeCache.keys(): |
567 |
if a.startswith(path+'/'): |
568 |
if recr: |
569 |
self.unlink(a, online) |
570 |
else: |
571 |
e = OSError("Directory not empty") |
572 |
e.errno = ENOTEMPTY |
573 |
raise e |
574 |
else: |
575 |
log.error("%s is not a known directory", path) |
576 |
e = OSError("No such folder"+path) |
577 |
e.errno = ENOENT |
578 |
raise e |
579 |
|
580 |
if path=='/sets' or path=='/tags' or path=='/tags/personal' \ |
581 |
or path=='/tags/public' or path=='/stream': |
582 |
log.debug("attempt to remove framework file %s rejected", path) |
583 |
e = OSError("removal of folder %s not allowed" % (path)) |
584 |
e.errno = EPERM |
585 |
raise e |
586 |
|
587 |
ind = path.rindex('/') |
588 |
pPath = path[:ind] |
589 |
inode = self.inodeCache.pop(path) |
590 |
if online and path.startswith('/sets/'): |
591 |
self.transfl.deleteSet(inode.setId) |
592 |
del inode |
593 |
pInode = self.getInode(pPath) |
594 |
pInode.nlink -= 1 |
595 |
self.updateInode(pPath, pInode) |
596 |
|
597 |
def symlink(self, path, path1): |
598 |
log.debug("symlink") |
599 |
return os.symlink(path, path1) |
600 |
|
601 |
def rename(self, path, path1): |
602 |
log.debug("%s %s", path, path1) |
603 |
#Donot allow Vim to create a file~ |
604 |
#Check for .meta in both paths |
605 |
if path.count('~')>0 or path1.count('~')>0: |
606 |
log.debug("vim enablement path entered") |
607 |
try: |
608 |
#Get inode, but _dont_ remove from cache |
609 |
inode = self.getInode(path) |
610 |
if inode is not None: |
611 |
self.inodeCache[path1] = inode |
612 |
except: |
613 |
log.debug("couldn't find inode for %s", path) |
614 |
return |
615 |
|
616 |
#Read from path |
617 |
inode = self.getInode(path) |
618 |
if inode is None or not hasattr(inode, 'photoId'): |
619 |
return |
620 |
fname = os.path.join(flickrfsHome, '.'+inode.photoId) |
621 |
f = open(fname, 'r') |
622 |
buf = f.read() |
623 |
f.close() |
624 |
|
625 |
#Now write to path1 |
626 |
inode = self.getInode(path1) |
627 |
if inode is None or not hasattr(inode, 'photoId'): |
628 |
return |
629 |
fname = os.path.join(flickrfsHome, '.'+inode.photoId) |
630 |
f = open(fname, 'w') |
631 |
f.write(buf) |
632 |
f.close() |
633 |
inode.size = os.path.getsize(fname) |
634 |
self.updateInode(path1, inode) |
635 |
retinfo = self.parse(fname, inode.photoId) |
636 |
if retinfo.count('Error')>0: |
637 |
log.error(retinfo) |
638 |
|
639 |
def link(self, srcpath, destpath): |
640 |
log.debug("%s %s", srcpath, destpath) |
641 |
#Add image from stream to set, w/o retrieving |
642 |
slist = srcpath.split('/') |
643 |
sname_file = slist.pop(-1) |
644 |
dlist = destpath.split('/') |
645 |
dname_file = dlist.pop(-1) |
646 |
error = 0 |
647 |
if sname_file=="" or sname_file.startswith('.'): |
648 |
error = 1 |
649 |
if dname_file=="" or dname_file.startswith('.'): |
650 |
error = 1 |
651 |
if not destpath.startswith('/sets/'): |
652 |
error = 1 |
653 |
if error is 1: |
654 |
log.error("linking is allowed only between 2 image files") |
655 |
return |
656 |
sinode = self.getInode(srcpath) |
657 |
self._mkfile(destpath, id=sinode.id, mode=sinode.mode, |
658 |
comm_meta=sinode.comm_meta, mtime=sinode.mtime, |
659 |
ctime=sinode.ctime) |
660 |
parentPath = '/'.join(dlist) |
661 |
pinode = self.getInode(parentPath) |
662 |
if pinode.setId==0: |
663 |
try: |
664 |
pinode.setId = self.transfl.createSet(parentPath, sinode.photoId) |
665 |
self.updateInode(parentPath, pinode) |
666 |
except: |
667 |
e = OSError("Can't create a new set") |
668 |
e.errno = EIO |
669 |
raise e |
670 |
else: |
671 |
self.transfl.put2Set(pinode.setId, sinode.photoId) |
672 |
|
673 |
|
674 |
def chmod(self, path, mode): |
675 |
log.debug("%s %s" % path, mode) |
676 |
inode = self.getInode(path) |
677 |
typesinfo = mimetypes.guess_type(path) |
678 |
|
679 |
if inode.comm_meta is None: |
680 |
log.debug("chmod on directory ignored") |
681 |
return |
682 |
|
683 |
elif typesinfo[0] is None or typesinfo[0].count('image')<=0: |
684 |
|
685 |
os.chmod(path, mode) |
686 |
return |
687 |
|
688 |
elif self.transfl.setPerm(inode.photoId, mode, inode.comm_meta)==True: |
689 |
inode.mode = mode |
690 |
self.updateInode(path, inode) |
691 |
return |
692 |
|
693 |
def chown(self, path, user, group): |
694 |
log.debug("%s:%s %s (ignored)", user, group, path) |
695 |
|
696 |
def truncate(self, path, size): |
697 |
log.debug("%s %s", path, size) |
698 |
ind = path.rindex('/') |
699 |
name_file = path[ind+1:] |
700 |
|
701 |
typeinfo = mimetypes.guess_type(path) |
702 |
if typeinfo[0] is None or typeinfo[0].count('image')<=0: |
703 |
inode = self.getInode(path) |
704 |
filePath = os.path.join(flickrfsHome, '.'+inode.photoId) |
705 |
f = open(filePath, 'w+') |
706 |
return f.truncate(size) |
707 |
|
708 |
def mknod(self, path, mode, dev): |
709 |
log.debug("%s %s %s ", path, mode, dev) |
710 |
templist = path.split('/') |
711 |
name_file = templist[-1] |
712 |
|
713 |
if name_file.startswith('.') and name_file.count('.meta') > 0: |
714 |
# We need to handle the special case, where some meta files are being |
715 |
# created through mknod. Creation of meta files is done when adding |
716 |
# images automatically; and they should not go through mknod system call. |
717 |
# Editors like Vim, try to generate random swap files when reading |
718 |
# meta; and this should be *disallowed*. |
719 |
log.debug("mknod for meta file %s ignored", path) |
720 |
return |
721 |
|
722 |
if path.startswith('/sets/'): |
723 |
templist[2] = templist[2].split(':')[0] |
724 |
elif path.startswith('/stream'): |
725 |
templist[1] = templist[1].split(':')[0] |
726 |
path = '/'.join(templist) |
727 |
|
728 |
log.debug("modified file path %s", path) |
729 |
#Lets guess what kind of a file is this. |
730 |
#Is it an image file? or, some other temporary file |
731 |
#created by the tools you're using. |
732 |
typeinfo = mimetypes.guess_type(path) |
733 |
if typeinfo[0] is None or typeinfo[0].count('image') <= 0: |
734 |
f = open(os.path.join(flickrfsHome,'.'+name_file), 'w') |
735 |
f.close() |
736 |
# TODO(manishrjain): This should not be FileInode, it should rather be |
737 |
# Inode. |
738 |
self.inodeCache[path] = inodes.FileInode(path, name_file, mode=mode) |
739 |
else: |
740 |
self._mkfile(path, id="NEW", mode=mode) |
741 |
|
742 |
def mkdir(self, path, mode): |
743 |
log.debug("%s with mode %s", path, mode) |
744 |
if path.startswith("/tags"): |
745 |
if path.count('/')==3: #/tags/personal (or private)/dirname ONLY |
746 |
self._mkdir(path) |
747 |
background(self.tags_thread, path) |
748 |
else: |
749 |
e = OSError("Not allowed to create directory %s" % path) |
750 |
e.errno = EACCES |
751 |
raise e |
752 |
elif path.startswith("/sets"): |
753 |
if path.count('/')==2: #Only allow creation of new set /sets/newset |
754 |
self._mkdir(path, id=0) |
755 |
#id=0 means that not yet created online |
756 |
else: |
757 |
e = OSError("Not allowed to create directory %s" % path) |
758 |
e.errno = EACCES |
759 |
raise e |
760 |
elif path=='/stream': |
761 |
self._mkdir(path) |
762 |
background(timerThread, self.stream_thread, |
763 |
self.sync_stream_thread, stream_sync_int) |
764 |
|
765 |
else: |
766 |
e = OSError("Not allowed to create directory %s" % path) |
767 |
e.errno = EACCES |
768 |
raise e |
769 |
|
770 |
def utime(self, path, times): |
771 |
inode = self.getInode(path) |
772 |
inode.atime = times[0] |
773 |
inode.mtime = times[1] |
774 |
self.updateInode(path, inode) |
775 |
return 0 |
776 |
|
777 |
def open(self, path, flags): |
778 |
log.info("%s with flags %s", path, flags) |
779 |
ind = path.rindex('/') |
780 |
name_file = path[ind+1:] |
781 |
if name_file.startswith('.') and name_file.endswith('.meta'): |
782 |
self.handleAccessToNonImage(path) |
783 |
return 0 |
784 |
typesinfo = mimetypes.guess_type(path) |
785 |
if typesinfo[0] is None or typesinfo[0].count('image')<=0: |
786 |
log.debug('non-image file found %s', path) |
787 |
self.handleAccessToNonImage(path) |
788 |
return 0 |
789 |
|
790 |
templist = path.split('/') |
791 |
if path.startswith('/sets/'): |
792 |
templist[2] = templist[2].split(':')[0] |
793 |
elif path.startswith('/stream'): |
794 |
templist[1] = templist[1].split(':')[0] |
795 |
path = '/'.join(templist) |
796 |
log.debug("path after modification is %s", path) |
797 |
|
798 |
inode = self.getInode(path) |
799 |
if inode.photoId=="NEW": #Just skip if new (i.e. uploading) |
800 |
return 0 |
801 |
if self.imgCache.getBuffer(inode.photoId)=="": |
802 |
log.debug("retrieving image %s from flickr", inode.photoId) |
803 |
self.imgCache.setBuffer(inode.photoId, |
804 |
str(self.transfl.getPhoto(inode.photoId))) |
805 |
inode.size = self.imgCache.getBufLen(inode.photoId) |
806 |
log.debug("size of image is %s", inode.size) |
807 |
self.updateInode(path, inode) |
808 |
return 0 |
809 |
|
810 |
def read(self, path, length, offset): |
811 |
log.debug("%s offset %s length %s", path, offset, length) |
812 |
ind = path.rindex('/') |
813 |
name_file = path[ind+1:] |
814 |
if name_file.startswith('.') and name_file.endswith('.meta'): |
815 |
# Check if file is not present. If not, retrieve and |
816 |
# create the file locally. |
817 |
buf = self.handleReadNonImage(path, length, offset) |
818 |
return buf |
819 |
typesinfo = mimetypes.guess_type(path) |
820 |
if typesinfo[0] is None or typesinfo[0].count('image')<=0: |
821 |
return self.handleReadNonImage(path, length, offset) |
822 |
return self.handleReadImage(path, length, offset) |
823 |
|
824 |
def parse(self, fname, photoId): |
825 |
cp = ConfigParser.ConfigParser() |
826 |
log.debug("parsing file %s for photoid %s", fname, photoId) |
827 |
cp.read(fname) |
828 |
log.debug("file %s has been read by ConfigParser", fname) |
829 |
options = cp.options('metainfo') |
830 |
title='' |
831 |
desc='' |
832 |
tags='' |
833 |
license='' |
834 |
if 'description' in options: |
835 |
desc = cp.get('metainfo', 'description') |
836 |
if 'tags' in options: |
837 |
tags = cp.get('metainfo', 'tags') |
838 |
if 'title' in options: |
839 |
title = cp.get('metainfo', 'title') |
840 |
if 'license' in options: |
841 |
license = cp.get('metainfo', 'license') |
842 |
|
843 |
log.debug("setting metadata for file %s", fname) |
844 |
if self.transfl.setMeta(photoId, title, desc)==False: |
845 |
return "Error:Can't set Meta information" |
846 |
|
847 |
log.debug("setting tags for %s", fname) |
848 |
if self.transfl.setTags(photoId, tags)==False: |
849 |
log.debug("setting tags for %s failed", fname) |
850 |
return "Error:Can't set tags" |
851 |
|
852 |
log.debug("setting license for %s", fname) |
853 |
if self.transfl.setLicense(photoId, license)==False: |
854 |
return "Error:Can't set license" |
855 |
|
856 |
# except: |
857 |
# log.error("Can't parse file:%s:"%(fname,)) |
858 |
# return "Error:Can't parse" |
859 |
return 'Success:Updated photo:%s:%s:'%(fname,photoId) |
860 |
|
861 |
################################################## |
862 |
# 'handle' Functions for handling read and writes. |
863 |
################################################## |
864 |
def handleAccessToNonImage(self, path): |
865 |
inode = self.getInode(path) |
866 |
if inode is None: |
867 |
log.error("inode %s doesn't exist", path) |
868 |
e = OSError("No inode found") |
869 |
e.errno = EIO |
870 |
raise e |
871 |
fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext |
872 |
# Handle the case when file already exists. |
873 |
if not os.path.exists(fname) or os.path.getsize(fname) == 0L: |
874 |
log.info("retrieving meta information for file %s and photo id %s", |
875 |
fname, inode.photoId) |
876 |
INFO = self.transfl.getPhotoInfo(inode.photoId) |
877 |
size = self.writeMetaInfo(inode.photoId, INFO) |
878 |
log.info("information has been written for photo id %s", inode.photoId) |
879 |
inode.size = size |
880 |
self.updateInode(path, inode) |
881 |
time.sleep(1) # Enough time for OS to call for getattr again. |
882 |
return inode |
883 |
|
884 |
def handleReadNonImage(self, path, length, offset): |
885 |
inode = self.handleAccessToNonImage(path) |
886 |
f = open(os.path.join(flickrfsHome, '.'+inode.photoId), 'r') |
887 |
f.seek(offset) |
888 |
return f.read(length) |
889 |
|
890 |
def handleReadImage(self, path, length, offset): |
891 |
inode = self.getInode(path) |
892 |
if inode is None: |
893 |
log.error("inode %s doesn't exist", path) |
894 |
e = OSError("No inode found") |
895 |
e.errno = EIO |
896 |
raise e |
897 |
if self.imgCache.getBufLen(inode.photoId) is 0: |
898 |
log.debug("retrieving image %s from flickr", inode.photoId) |
899 |
buf = retryFlickrOp(False, self.transfl.getPhoto, |
900 |
inode.photoId) |
901 |
if len(buf) == 0: |
902 |
log.error("can't retrieve image %s", inode.photoId) |
903 |
e = OSError("Unable to retrieve image.") |
904 |
e.errno = EIO |
905 |
raise e |
906 |
self.imgCache.setBuffer(inode.photoId, buf) |
907 |
inode.size = self.imgCache.getBufLen(inode.photoId) |
908 |
temp = self.imgCache.getBuffer(inode.photoId, offset, offset+length) |
909 |
if len(temp) < length: |
910 |
self.imgCache.popBuffer(inode.photoId) |
911 |
self.updateInode(path, inode) |
912 |
return temp |
913 |
|
914 |
def handleWriteToNonImage(self, path, buf, off): |
915 |
inode = self.handleAccessToNonImage(path) |
916 |
fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext |
917 |
log.debug("writing to %s", fname) |
918 |
f = open(fname, 'r+') |
919 |
f.seek(off) |
920 |
f.write(buf) |
921 |
f.close() |
922 |
if len(buf)<4096: |
923 |
inode.size = os.path.getsize(fname) |
924 |
retinfo = self.parse(fname, inode.photoId) |
925 |
if retinfo.count('Error')>0: |
926 |
e = OSError(retinfo.split(':')[1]) |
927 |
e.errno = EIO |
928 |
raise e |
929 |
self.updateInode(path, inode) |
930 |
return len(buf) |
931 |
|
932 |
def handleUploadingImage(self, path, inode, taglist): |
933 |
tags = [ '"%s"'%(a,) for a in taglist] |
934 |
tags.append('flickrfs') |
935 |
taglist = ' '.join(tags) |
936 |
log.info('uploading %s with len %s', |
937 |
path, self.imgCache.getBufLen(inode.photoId)) |
938 |
id = None |
939 |
bufData = self.imgCache.getBuffer(inode.photoId) |
940 |
bufData = self.imageResize(bufData) |
941 |
id = retryFlickrOp(False, self.transfl.uploadfile, |
942 |
path, taglist, bufData, inode.mode) |
943 |
if id is None: |
944 |
log.error("unable to upload file %s", inode.photoId) |
945 |
e = OSError("Unable to upload file.") |
946 |
e.errno = EIO |
947 |
raise e |
948 |
self.imgCache.popBuffer(inode.photoId) |
949 |
inode.photoId = id |
950 |
self.updateInode(path, inode) |
951 |
return inode |
952 |
|
953 |
def handleWriteToBuffer(self, path, buf): |
954 |
inode = self.getInode(path) |
955 |
if inode is None: |
956 |
log.error("inode %s doesn't exist", path) |
957 |
e = OSError("No inode found") |
958 |
e.errno = EIO |
959 |
raise e |
960 |
self.imgCache.addBuffer(inode.photoId, buf) |
961 |
return inode |
962 |
|
963 |
def handleWriteAddToSet(self, parentPath, pinode, inode): |
964 |
#Create set if it doesn't exist online (i.e. if id=0) |
965 |
if pinode.setId is 0: |
966 |
# Retry creation of set if unsuccessful. |
967 |
pinode.setId = retryFlickrOp(False, self.transfl.createSet, |
968 |
parentPath, inode.photoId) |
969 |
# If the set is created, then return. |
970 |
if pinode.setId is not None: |
971 |
self.updateInode(parentPath, pinode) |
972 |
return |
973 |
else: |
974 |
log.error("unable to create set %s", parentPath) |
975 |
e = OSError("Unable to create set.") |
976 |
e.errno = EIO |
977 |
raise e |
978 |
else: |
979 |
# If the operation put2Set doesn't throw exception, that means |
980 |
# that the picture has been successfully added to set. |
981 |
# Return in that case, retry otherwise. |
982 |
retryFlickrOp(True, self.transfl.put2Set, |
983 |
pinode.setId, inode.photoId) |
984 |
return |
985 |
|
986 |
############################# |
987 |
# End of 'handle' Functions. |
988 |
############################# |
989 |
|
990 |
def write(self, path, buf, off): |
991 |
log.debug("write to %s at offset %s", path, off) |
992 |
ind = path.rindex('/') |
993 |
name_file = path[ind+1:] |
994 |
if name_file.startswith('.') and name_file.count('.meta')>0: |
995 |
return self.handleWriteToNonImage(path, buf, off) |
996 |
typesinfo = mimetypes.guess_type(path) |
997 |
if typesinfo[0] is None or typesinfo[0].count('image')<=0: |
998 |
return self.handleWriteToNonImage(path, buf, off) |
999 |
templist = path.split('/') |
1000 |
inode = None |
1001 |
if path.startswith('/tags'): |
1002 |
e = OSError("Copying to tags not allowed") |
1003 |
e.errno = EIO |
1004 |
raise e |
1005 |
if path.startswith('/stream'): |
1006 |
tags = templist[1].split(':') |
1007 |
templist[1] = tags.pop(0) |
1008 |
path = '/'.join(templist) |
1009 |
inode = self.handleWriteToBuffer(path, buf) |
1010 |
if len(buf) < 4096: |
1011 |
self.handleUploadingImage(path, inode, tags) |
1012 |
elif path.startswith('/sets/'): |
1013 |
setnTags = templist[2].split(':') |
1014 |
setName = setnTags.pop(0) |
1015 |
templist[2] = setName |
1016 |
path = '/'.join(templist) |
1017 |
inode = self.handleWriteToBuffer(path, buf) |
1018 |
if len(buf) < 4096: |
1019 |
templist.pop(-1) |
1020 |
parentPath = '/'.join(templist) |
1021 |
pinode = self.getInode(parentPath) |
1022 |
inode = self.handleUploadingImage(path, inode, setnTags) |
1023 |
self.handleWriteAddToSet(parentPath, pinode, inode) |
1024 |
log.debug("done write to %s at offset %s", path, off) |
1025 |
if len(buf)<4096: |
1026 |
templist = path.split('/') |
1027 |
templist.pop(-1) |
1028 |
parentPath = '/'.join(templist) |
1029 |
try: |
1030 |
self.inodeCache.pop(path) |
1031 |
except: |
1032 |
pass |
1033 |
INFO = self.transfl.getPhotoInfo(inode.photoId) |
1034 |
info = self.transfl.parseInfoFromFullInfo(inode.photoId, INFO) |
1035 |
self._mkfileWithMeta(parentPath, info) |
1036 |
self.writeMetaInfo(inode.photoId, INFO) |
1037 |
return len(buf) |
1038 |
|
1039 |
def getInode(self, path): |
1040 |
if self.inodeCache.has_key(path): |
1041 |
#log.debug("got cached inode for %s", path) |
1042 |
return self.inodeCache[path] |
1043 |
else: |
1044 |
#log.debug("%s is not in inode cache", path) |
1045 |
return None |
1046 |
|
1047 |
def updateInode(self, path, inode): |
1048 |
self.inodeCache[path] = inode |
1049 |
|
1050 |
def release(self, path, flags): |
1051 |
log.debug("%s with flags %s ignored", path, flags) |
1052 |
return 0 |
1053 |
|
1054 |
def statfs(self): |
1055 |
""" |
1056 |
Should return a tuple with the following elements in respective order: |
1057 |
|
1058 |
F_BSIZE - Preferred file system block size. (int) |
1059 |
F_FRSIZE - Fundamental file system block size. (int) |
1060 |
F_BLOCKS - Total number of blocks in the filesystem. (long) |
1061 |
F_BFREE - Total number of free blocks. (long) |
1062 |
F_BAVAIL - Free blocks available to non-super user. (long) |
1063 |
F_FILES - Total number of file nodes. (long) |
1064 |
F_FFREE - Total number of free file nodes. (long) |
1065 |
F_FAVAIL - Free nodes available to non-super user. (long) |
1066 |
F_FLAG - Flags. System dependent: see statvfs() man page. (int) |
1067 |
F_NAMEMAX - Maximum file name length. (int) |
1068 |
Feel free to set any of the above values to 0, which tells |
1069 |
the kernel that the info is not available. |
1070 |
""" |
1071 |
block_size = 1024 |
1072 |
blocks = 0L |
1073 |
blocks_free = 0L |
1074 |
files = 0L |
1075 |
files_free = 0L |
1076 |
namelen = 255 |
1077 |
# statfs is being called repeatedly at least once a second. |
1078 |
# The bandwidth information doesn't change that often, so |
1079 |
# save upon communication with flickr servers to retrieve this |
1080 |
# information. Only retrieve it once in a while. |
1081 |
if self.statfsCounter >= 500 or self.statfsCounter is -1: |
1082 |
(self.max, self.used) = self.transfl.getBandwidthInfo() |
1083 |
self.statfsCounter = 0 |
1084 |
log.info('retrieved bandwidth info: max %s used %s', |
1085 |
self.max, self.used) |
1086 |
self.statfsCounter = self.statfsCounter + 1 |
1087 |
|
1088 |
if self.max is not None: |
1089 |
blocks = long(self.max)/block_size |
1090 |
blocks_used = long(self.used)/block_size |
1091 |
blocks_free = blocks - blocks_used |
1092 |
blocks_available = blocks_free |
1093 |
return (block_size, blocks, blocks_free, blocks_available, |
1094 |
files, files_free, namelen) |
1095 |
|
1096 |
def fsync(self, path, isfsyncfile): |
1097 |
log.debug("%s, isfsyncfile=%s", path, isfsyncfile) |
1098 |
return 0 |
1099 |
|
1100 |
|
1101 |
if __name__ == '__main__': |
1102 |
try: |
1103 |
server = Flickrfs() |
1104 |
server.multithreaded = 1; |
1105 |
server.main() |
1106 |
except KeyError: |
1107 |
log.error('got key error; exiting...') |
1108 |
sys.exit(0) |