diff -Naur flickrfs-1.3.9.orig/COPYING flickrfs-cvs.orig/COPYING --- flickrfs-1.3.9.orig/COPYING 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/COPYING 2007-03-29 16:44:24.000000000 +0200 @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff -Naur flickrfs-1.3.9.orig/flickrapi.py flickrfs-cvs.orig/flickrapi.py --- flickrfs-1.3.9.orig/flickrapi.py 2007-02-04 06:08:09.000000000 +0100 +++ flickrfs-cvs.orig/flickrapi.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,531 +0,0 @@ -#!/usr/bin/python -# -# Flickr API implementation -# -# Inspired largely by Michele Campeotto's flickrclient and Aaron Swartz' -# xmltramp... but I wanted to get a better idea of how python worked in -# those regards, so I mostly worked those components out for myself. -# -# http://micampe.it/things/flickrclient -# http://www.aaronsw.com/2002/xmltramp/ -# -# Release 1: initial release -# Release 2: added upload functionality -# Release 3: code cleanup, convert to doc strings -# Release 4: better permission support -# Release 5: converted into fuller-featured "flickrapi" -# Release 6: fix upload sig bug (thanks Deepak Jois), encode test output -# -# Copyright 2005 Brian "Beej Jorgensen" Hall beej@beej.us -# -# This work is licensed under the Creative Commons -# Attribution License. To view a copy of this license, -# visit http://creativecommons.org/licenses/by/2.5/ or send -# a letter to Creative Commons, 543 Howard Street, 5th -# Floor, San Francisco, California, 94105, USA. -# -# This license says that I must be credited for any derivative works. -# You do not need to credit me to simply use the FlickrAPI classes in -# your Python scripts-- you only need to credit me if you're taking this -# FlickrAPI class and transforming it into your own. -# -# Previous versions of this API were granted to the public domain. -# You're free to use those as you please. -# -# Beej Jorgensen, August 2005 -# beej@beej.us -#------------------------------------------ -# Modified(debugged portions + added functionality) by Manish Rai Jain -# If you are interested in finding out exactly -# what portions I have modified, just search for 'manish'. Each change is -# tagged with my name for easy locate. -# Additional modifications similarly tagged by R. David Murray - -import sys -import md5 -import string -import urllib -import httplib -import os.path -import xml.dom.minidom -import urllib2 -import socket -DEBUG = 0 -######################################################################## -# XML functionality -######################################################################## - -#----------------------------------------------------------------------- -class XMLNode: - """XMLNode -- generic class for holding an XML node - xmlStr = \"\"\" - Name0 - Name1 - \"\"\" - - f = XMLNode.parseXML(xmlStr) - - print f.elementName - print f['foo'] - print f.name - print f.name[0].elementName - print f.name[0]["bar"] - print f.name[0].elementText - print f.name[1].elementName - print f.name[1]["bar"] - print f.name[1]["baz"] - - """ - - def __init__(self): - """Construct an empty XML node.""" - self.elementName="" - self.elementText="" - self.attrib={} - self.xml="" - - def __setitem__(self, key, item): - """Store a node's attribute in the attrib hash.""" - self.attrib[key] = item - - def __getitem__(self, key): - """Retrieve a node's attribute from the attrib hash.""" - return self.attrib[key] - - # Modified here: add a couple of methods to make it even easier to handle errors. - # Mod by: R. David Murray - def __nonzero__(self): - if self['stat'] == "fail": return False - return True - - def get_errortext(self): - if self: return '' - return "%s: error %s: %s\n" % (self.elementName, \ - self.err[0]['code'], self.err[0]['msg']) - errormsg = property(get_errortext) - - #----------------------------------------------------------------------- - #@classmethod - def parseXML(cls, xmlStr="", storeXML=False): - """Convert an XML string into a nice instance tree of XMLNodes. - - xmlStr -- the XML to parse - storeXML -- if True, stores the XML string in the root XMLNode.xml - - """ - - def __parseXMLElement(element, thisNode): - """Recursive call to process this XMLNode.""" - thisNode.elementName = element.nodeName - - #print element.nodeName - - # add element attributes as attributes to this node - for i in range(element.attributes.length): - an = element.attributes.item(i) - thisNode[an.name] = an.nodeValue - - for a in element.childNodes: - if a.nodeType == xml.dom.Node.ELEMENT_NODE: - - child = XMLNode() - try: - list = getattr(thisNode, a.nodeName) - except AttributeError: - setattr(thisNode, a.nodeName, []) - - # add the child node as an attrib to this node - list = getattr(thisNode, a.nodeName); - #print "appending child: %s to %s" % (a.nodeName, thisNode.elementName) - list.append(child); - - __parseXMLElement(a, child) - - elif a.nodeType == xml.dom.Node.TEXT_NODE: - thisNode.elementText += a.nodeValue - - return thisNode - dom = xml.dom.minidom.parseString(xmlStr) - - # get the root - rootNode = XMLNode() - if storeXML: rootNode.xml = xmlStr - - return __parseXMLElement(dom.firstChild, rootNode) - -######################################################################## -# Flickr functionality -######################################################################## - -#----------------------------------------------------------------------- -class FlickrAPI: - """Encapsulated flickr functionality. - - Example usage: - - flickr = FlickrAPI(flickrAPIKey, flickrSecret) - rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token) - - """ - flickrHost = "flickr.com" - flickrRESTForm = "/services/rest/" - flickrAuthForm = "/services/auth/" - flickrUploadForm = "/services/upload/" - #------------------------------------------------------------------- - def __init__(self, apiKey, secret): - """Construct a new FlickrAPI instance for a given API key and secret.""" - self.apiKey = apiKey - self.secret = secret - - self.__handlerCache={} - socket.setdefaulttimeout(10) - #------------------------------------------------------------------- - def __sign(self, data): - """Calculate the flickr signature for a set of params. - - data -- a hash of all the params and values to be hashed, e.g. - {"api_key":"AAAA", "auth_token":"TTTT"} - - """ - dataName = "" - if self.secret is not None: - dataName = self.secret - #print data - keys = data.keys() - keys.sort() - - for a in keys: - if data[a] is not None: - dataName += "%s%s" % (a, data[a]) - #print 'dataName:', dataName - hash = md5.new() - hash.update(dataName) - return hash.hexdigest() - - #------------------------------------------------------------------- - def __getattr__(self, method, **arg): - """Handle all the flickr API calls. - - This is Michele Campeotto's cleverness, wherein he writes a - general handler for methods not defined, and assumes they are - flickr methods. He then converts them to a form to be passed as - the method= parameter, and goes from there. - - http://micampe.it/things/flickrclient - - My variant is the same basic thing, except it tracks if it has - already created a handler for a specific call or not. - - example usage: - - flickr.auth_getFrob(api_key="AAAAAA") - rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\ - auth_token=token) - - """ - - if not self.__handlerCache.has_key(method): - def handler(_self = self, _method = method, **arg): - _method = "flickr." + _method.replace("_", ".") - url = "http://" + FlickrAPI.flickrHost + \ - FlickrAPI.flickrRESTForm - arg["method"] = _method - # Modified here: use default api_key and auth_token if not supplied - # Mod by: R. David Murray - - #API Key is used in all the methods. So, instead of specifying it as - #parameter in calling function, we can append it over here by default. - #Token eq. to Authentication is not required for ALL methods. So, - #better specify when needed. -Manish - - if not 'api_key' in arg: arg["api_key"] = _self.apiKey -# if not 'auth_token' in arg and hasattr(_self, 'token'): arg['auth_token'] = _self.token - - postData = str(urllib.urlencode(arg)) + "&api_sig=" + \ - str(_self.__sign(arg)) - if DEBUG: - print "--url---------------------------------------------" - print url - print "--postData----------------------------------------" - print postData - data = '' - req = urllib2.Request(url, postData) -# socket.defaulttimetout(60) - f = urllib2.urlopen(req) - data = f.read() - if DEBUG: - print "--response----------------------------------------" - print data - f.close() - tempNode = XMLNode() - # Modified here: added XMLNode() as the 1st argument - # Mod by: Manish Rai Jain - return XMLNode.parseXML(XMLNode(),xmlStr=data, storeXML=True) - - self.__handlerCache[method] = handler; - - return self.__handlerCache[method] - - #------------------------------------------------------------------- - def __getAuthURL(self, perms, frob): - """Return the authorization URL to get a token. - - This is the URL the app will launch a browser toward if it - needs a new token. - - perms -- "read", "write", or "delete" - frob -- picked up from an earlier call to FlickrAPI.auth_getFrob() - - """ - - data = {"api_key": self.apiKey, "frob": frob, "perms": perms} - data["api_sig"] = self.__sign(data) - return "http://%s%s?%s" % (FlickrAPI.flickrHost, \ - FlickrAPI.flickrAuthForm, urllib.urlencode(data)) - - #------------------------------------------------------------------- - def upload(self, filename, jpegData="", **arg): - """Upload a file to flickr. - - jpegData -- send buffered data read from file instead of filename - - Be extra careful you spell the parameters correctly, or you will - get a rather cryptic "Invalid Signature" error on the upload! - - Supported parameters: - - api_key - auth_token -- documentation mistakenly calls this "auth_hash" - title - description - tags -- space-delimited list of tags, "tag1 tag2 tag3" - is_public -- "1" or "0" - is_friend -- "1" or "0" - is_family -- "1" or "0" - - """ - - # verify key names - for a in arg.keys(): - if a != "api_key" and a != "auth_token" and a != "title" and \ - a != "description" and a != "tags" and a != "is_public" and \ - a != "is_friend" and a != "is_family": - - sys.stderr.write("FlickrAPI: warning: unknown parameter " \ - "\"%s\" sent to FlickrAPI.upload\n" % (a)) - - # Modified here: use default api_key and auth_token if not supplied - # Mod by: R. David Murray - if not 'api_key' in arg: arg["api_key"] = self.apiKey - if not 'auth_token' in arg: arg['auth_token'] = self.token - arg["api_sig"] = self.__sign(arg) - url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm - - # construct POST data -# boundary = "===beej=jorgensen==========7d45e178b0434" - import mimetools - boundary = mimetools.choose_boundary() - Hdr = "multipart/form-data; boundary=%s" % boundary - body = "" - - # required params - for a in ('api_key', 'auth_token', 'api_sig'): - body += "--%s\r\n" % (boundary) - body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" - body += "%s\r\n" % (arg[a]) - - # optional params - for a in ('title', 'description', 'tags', 'is_public', \ - 'is_friend', 'is_family'): - - if arg.has_key(a): - body += "--%s\r\n" % (boundary) - body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" - body += "%s\r\n" % (arg[a]) - - body += "--%s\r\n" % (boundary) - body += "Content-Disposition: form-data; name=\"photo\";" - body += " filename=\"%s\"\r\n" % filename - body += "Content-Type: image/jpeg\r\n\r\n" - - #print body - - try: - # Added by Manish for allowing upload - # by sending buffer instead of specifying filename - if jpegData=="": - fp = file(filename, "rb") - jpegData = fp.read() - fp.close() - - postData = body.encode("utf_8") + jpegData + "\r\n" + \ - ("--%s--" % (boundary)).encode("utf_8") - - - # Modified by Manish for allowing - # upload through proxy - - request = urllib2.Request(url) - request.add_data(postData) - request.add_header("Content-Type", Hdr) - response = urllib2.urlopen(request) - rspXML = response.read() - # Modified by Manish Rai Jain - return XMLNode.parseXML(XMLNode(), xmlStr=rspXML) - except IOError: - return None - - - - #----------------------------------------------------------------------- - #@classmethod - def testFailure(cls, rsp, exit=True): - """Exit app if the rsp XMLNode indicates failure.""" - if rsp['stat'] == "fail": - sys.stderr.write("%s: error %s: %s\n" % (rsp.elementName, \ - rsp.err[0]['code'], rsp.err[0]['msg'])) - if exit: sys.exit(1) - - #----------------------------------------------------------------------- - def __getCachedTokenPath(self): - """Return the directory holding the app data.""" - return os.path.expanduser("~/.flickr/%s" % (self.apiKey)) - - #----------------------------------------------------------------------- - def __getCachedTokenFilename(self): - """Return the full pathname of the cached token file.""" - return "%s/auth.xml" % (self.__getCachedTokenPath()) - - #----------------------------------------------------------------------- - def __getCachedToken(self): - """Read and return a cached token, or None if not found. - - The token is read from the cached token file, which is basically the - entire RSP response containing the auth element. - """ - - try: - f = file(self.__getCachedTokenFilename(), "r") - data = f.read() - f.close() - # Modified here: added XMLNode() as the 1st argument - # Mod by: Manish Rai Jain - rsp = XMLNode.parseXML(XMLNode(), data) - - return rsp.auth[0].token[0].elementText - - except IOError: - return None - - #----------------------------------------------------------------------- - def __setCachedToken(self, xml): - """Cache a token for later use. - - The cached tag is stored by simply saving the entire RSP response - containing the auth element. - - """ - - path = self.__getCachedTokenPath() - if not os.path.exists(path): - os.makedirs(path) - - f = file(self.__getCachedTokenFilename(), "w") - f.write(xml) - f.close() - - - #----------------------------------------------------------------------- - def getToken(self, perms="write", browser="lynx"): - """Get a token either from the cache, or make a new one from the - frob. - - This first attempts to find a token in the user's token cache on - disk. - - If that fails (or if the token is no longer valid based on - flickr.auth.checkToken) a new frob is acquired. The frob is - validated by having the user log into flickr (with lynx), and - subsequently a valid token is retrieved. - - The newly minted token is then cached locally for the next run. - - perms--"read", "write", or "delete" - browser--whatever browser should be used in the system() call - - """ - - # see if we have a saved token - token = self.__getCachedToken() - - # see if it's valid - if token != None: - rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token) - if rsp['stat'] != "ok": - token = None - else: - # see if we have enough permissions - tokenPerms = rsp.auth[0].perms[0].elementText - if tokenPerms == "read" and perms != "read": token = None - elif tokenPerms == "write" and perms == "delete": token = None - - # get a new token if we need one - if token == None: - # get the frob - rsp = self.auth_getFrob(api_key=self.apiKey) - self.testFailure(rsp) - frob = rsp.frob[0].elementText - - # validate online - os.system("%s '%s'" % (browser, self.__getAuthURL(perms, frob))) - # get a token - rsp = self.auth_getToken(api_key=self.apiKey, frob=frob) - self.testFailure(rsp) - token = rsp.auth[0].token[0].elementText - - # store the auth info for next time - self.__setCachedToken(rsp.xml) - - # Modified here: save the auth token to use as a default - # Mod by: R. David Murray - self.token = token - return token - - -######################################################################## -# App functionality -######################################################################## - -def main(argv): - # flickr auth information: - flickrAPIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # API key - flickrSecret = "yyyyyyyyyyyyyyyy" # shared "secret" - - # make a new FlickrAPI instance - fapi = FlickrAPI(flickrAPIKey, flickrSecret) - - # do the whole whatever-it-takes to get a valid token: - token = fapi.getToken(browser="/opt/firefox/firefox") - - # get my favorites - rsp = fapi.favorites_getList(api_key=flickrAPIKey,auth_token=token) - fapi.testFailure(rsp) - - # and print them - for a in rsp.photos[0].photo: - print "%10s: %s" % (a['id'], a['title'].encode("ascii", "replace")) - - # upload the file foo.jpg - #rsp = fapi.upload("foo.jpg", api_key=flickrAPIKey, auth_token=token, \ - # title="This is the title", description="This is the description", \ - # tags="tag1 tag2 tag3", is_public="1") - #if rsp == None: - # sys.stderr.write("can't find file\n") - #else: - # fapi.testFailure(rsp) - - return 0 - -# run the main if we're not being imported: -if __name__ == "__main__": sys.exit(main(sys.argv)) - diff -Naur flickrfs-1.3.9.orig/flickrfs/flickrapi.py flickrfs-cvs.orig/flickrfs/flickrapi.py --- flickrfs-1.3.9.orig/flickrfs/flickrapi.py 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/flickrfs/flickrapi.py 2007-06-15 19:04:00.000000000 +0200 @@ -0,0 +1,549 @@ +# Flickr API implementation +# +# Inspired largely by Michele Campeotto's flickrclient and Aaron Swartz' +# xmltramp... but I wanted to get a better idea of how python worked in +# those regards, so I mostly worked those components out for myself. +# +# http://micampe.it/things/flickrclient +# http://www.aaronsw.com/2002/xmltramp/ +# +# Release 1: initial release +# Release 2: added upload functionality +# Release 3: code cleanup, convert to doc strings +# Release 4: better permission support +# Release 5: converted into fuller-featured "flickrapi" +# Release 6: fix upload sig bug (thanks Deepak Jois), encode test output +# Release 7: fix path construction, Manish Rai Jain's improvements, exceptions +# Release 8: change API endpoint to "api.flickr.com" +# Release 9: change to MIT license +# Release 10: fix horrid \r\n bug on final boundary +# Release 11: break out validateFrob() for subclassing +# +# Work by (or inspired by) Manish Rai Jain : +# +# improved error reporting, proper multipart MIME boundary creation, +# use of urllib2 to allow uploads through a proxy, upload accepts +# raw data as well as a filename +# +# Copyright (c) 2007 Brian "Beej Jorgensen" Hall +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Certain previous versions of this API were granted to the public +# domain. You're free to use those as you please. +# +# Beej Jorgensen, Maintainer, 19-Jan-2007 +# beej@beej.us +# +#------------------------------------------ +# Modified(debugged portions + added functionality) by Manish Rai Jain +# If you are interested in finding out exactly +# what portions I have modified, just search for 'manish'. Each change is +# tagged with my name for easy locate. +# Additional modifications similarly tagged by R. David Murray + +import sys +import md5 +import string +import urllib +import httplib +import os.path +import xml.dom.minidom +import urllib2 +import socket +DEBUG = 0 +######################################################################## +# XML functionality +######################################################################## + +#----------------------------------------------------------------------- +class XMLNode: + """XMLNode -- generic class for holding an XML node + xmlStr = \"\"\" + Name0 + Name1 + \"\"\" + + f = XMLNode.parseXML(xmlStr) + + print f.elementName + print f['foo'] + print f.name + print f.name[0].elementName + print f.name[0]["bar"] + print f.name[0].elementText + print f.name[1].elementName + print f.name[1]["bar"] + print f.name[1]["baz"] + + """ + + def __init__(self): + """Construct an empty XML node.""" + self.elementName="" + self.elementText="" + self.attrib={} + self.xml="" + + def __setitem__(self, key, item): + """Store a node's attribute in the attrib hash.""" + self.attrib[key] = item + + def __getitem__(self, key): + """Retrieve a node's attribute from the attrib hash.""" + return self.attrib[key] + + # Modified here: add a couple of methods to make it even easier to handle errors. + # Mod by: R. David Murray + def __nonzero__(self): + if self['stat'] == "fail": return False + return True + + def get_errortext(self): + if self: return '' + return "%s: error %s: %s\n" % (self.elementName, \ + self.err[0]['code'], self.err[0]['msg']) + errormsg = property(get_errortext) + + #----------------------------------------------------------------------- + #@classmethod + def parseXML(cls, xmlStr="", storeXML=False): + """Convert an XML string into a nice instance tree of XMLNodes. + + xmlStr -- the XML to parse + storeXML -- if True, stores the XML string in the root XMLNode.xml + + """ + + def __parseXMLElement(element, thisNode): + """Recursive call to process this XMLNode.""" + thisNode.elementName = element.nodeName + + #print element.nodeName + + # add element attributes as attributes to this node + for i in range(element.attributes.length): + an = element.attributes.item(i) + thisNode[an.name] = an.nodeValue + + for a in element.childNodes: + if a.nodeType == xml.dom.Node.ELEMENT_NODE: + + child = XMLNode() + try: + list = getattr(thisNode, a.nodeName) + except AttributeError: + setattr(thisNode, a.nodeName, []) + + # add the child node as an attrib to this node + list = getattr(thisNode, a.nodeName); + #print "appending child: %s to %s" % (a.nodeName, thisNode.elementName) + list.append(child); + + __parseXMLElement(a, child) + + elif a.nodeType == xml.dom.Node.TEXT_NODE: + thisNode.elementText += a.nodeValue + + return thisNode + dom = xml.dom.minidom.parseString(xmlStr) + + # get the root + rootNode = XMLNode() + if storeXML: rootNode.xml = xmlStr + + return __parseXMLElement(dom.firstChild, rootNode) + +######################################################################## +# Flickr functionality +######################################################################## + +#----------------------------------------------------------------------- +class FlickrAPI: + """Encapsulated flickr functionality. + + Example usage: + + flickr = FlickrAPI(flickrAPIKey, flickrSecret) + rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token) + + """ + flickrHost = "flickr.com" + flickrRESTForm = "/services/rest/" + flickrAuthForm = "/services/auth/" + flickrUploadForm = "/services/upload/" + #------------------------------------------------------------------- + def __init__(self, apiKey, secret): + """Construct a new FlickrAPI instance for a given API key and secret.""" + self.apiKey = apiKey + self.secret = secret + + self.__handlerCache={} + socket.setdefaulttimeout(10) + #------------------------------------------------------------------- + def __sign(self, data): + """Calculate the flickr signature for a set of params. + + data -- a hash of all the params and values to be hashed, e.g. + {"api_key":"AAAA", "auth_token":"TTTT"} + + """ + dataName = "" + if self.secret is not None: + dataName = self.secret + #print data + keys = data.keys() + keys.sort() + + for a in keys: + if data[a] is not None: + dataName += "%s%s" % (a, data[a]) + #print 'dataName:', dataName + hash = md5.new() + hash.update(dataName) + return hash.hexdigest() + + #------------------------------------------------------------------- + def __getattr__(self, method, **arg): + """Handle all the flickr API calls. + + This is Michele Campeotto's cleverness, wherein he writes a + general handler for methods not defined, and assumes they are + flickr methods. He then converts them to a form to be passed as + the method= parameter, and goes from there. + + http://micampe.it/things/flickrclient + + My variant is the same basic thing, except it tracks if it has + already created a handler for a specific call or not. + + example usage: + + flickr.auth_getFrob(api_key="AAAAAA") + rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\ + auth_token=token) + + """ + + if not self.__handlerCache.has_key(method): + def handler(_self = self, _method = method, **arg): + _method = "flickr." + _method.replace("_", ".") + url = "http://" + FlickrAPI.flickrHost + \ + FlickrAPI.flickrRESTForm + arg["method"] = _method + # Modified here: use default api_key and auth_token if not supplied + # Mod by: R. David Murray + + #API Key is used in all the methods. So, instead of specifying it as + #parameter in calling function, we can append it over here by default. + #Token eq. to Authentication is not required for ALL methods. So, + #better specify when needed. -Manish + + if not 'api_key' in arg: arg["api_key"] = _self.apiKey +# if not 'auth_token' in arg and hasattr(_self, 'token'): arg['auth_token'] = _self.token + + postData = str(urllib.urlencode(arg)) + "&api_sig=" + \ + str(_self.__sign(arg)) + if DEBUG: + print "--url---------------------------------------------" + print url + print "--postData----------------------------------------" + print postData + data = '' + req = urllib2.Request(url, postData) +# socket.defaulttimetout(60) + f = urllib2.urlopen(req) + data = f.read() + if DEBUG: + print "--response----------------------------------------" + print data + f.close() + tempNode = XMLNode() + # Modified here: added XMLNode() as the 1st argument + # Mod by: Manish Rai Jain + return XMLNode.parseXML(XMLNode(),xmlStr=data, storeXML=True) + + self.__handlerCache[method] = handler; + + return self.__handlerCache[method] + + #------------------------------------------------------------------- + def __getAuthURL(self, perms, frob): + """Return the authorization URL to get a token. + + This is the URL the app will launch a browser toward if it + needs a new token. + + perms -- "read", "write", or "delete" + frob -- picked up from an earlier call to FlickrAPI.auth_getFrob() + + """ + + data = {"api_key": self.apiKey, "frob": frob, "perms": perms} + data["api_sig"] = self.__sign(data) + return "http://%s%s?%s" % (FlickrAPI.flickrHost, \ + FlickrAPI.flickrAuthForm, urllib.urlencode(data)) + + #------------------------------------------------------------------- + def upload(self, filename, jpegData="", **arg): + """Upload a file to flickr. + + jpegData -- send buffered data read from file instead of filename + + Be extra careful you spell the parameters correctly, or you will + get a rather cryptic "Invalid Signature" error on the upload! + + Supported parameters: + + api_key + auth_token -- documentation mistakenly calls this "auth_hash" + title + description + tags -- space-delimited list of tags, "tag1 tag2 tag3" + is_public -- "1" or "0" + is_friend -- "1" or "0" + is_family -- "1" or "0" + + """ + + # verify key names + for a in arg.keys(): + if a != "api_key" and a != "auth_token" and a != "title" and \ + a != "description" and a != "tags" and a != "is_public" and \ + a != "is_friend" and a != "is_family": + + sys.stderr.write("FlickrAPI: warning: unknown parameter " \ + "\"%s\" sent to FlickrAPI.upload\n" % (a)) + + # Modified here: use default api_key and auth_token if not supplied + # Mod by: R. David Murray + if not 'api_key' in arg: arg["api_key"] = self.apiKey + if not 'auth_token' in arg: arg['auth_token'] = self.token + arg["api_sig"] = self.__sign(arg) + url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm + + # construct POST data +# boundary = "===beej=jorgensen==========7d45e178b0434" + import mimetools + boundary = mimetools.choose_boundary() + Hdr = "multipart/form-data; boundary=%s" % boundary + body = "" + + # required params + for a in ('api_key', 'auth_token', 'api_sig'): + body += "--%s\r\n" % (boundary) + body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" + body += "%s\r\n" % (arg[a]) + + # optional params + for a in ('title', 'description', 'tags', 'is_public', \ + 'is_friend', 'is_family'): + + if arg.has_key(a): + body += "--%s\r\n" % (boundary) + body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" + body += "%s\r\n" % (arg[a]) + + body += "--%s\r\n" % (boundary) + body += "Content-Disposition: form-data; name=\"photo\";" + body += " filename=\"%s\"\r\n" % filename + body += "Content-Type: image/jpeg\r\n\r\n" + + #print body + + try: + # Added by Manish for allowing upload + # by sending buffer instead of specifying filename + if jpegData=="": + fp = file(filename, "rb") + jpegData = fp.read() + fp.close() + + postData = body.encode("utf_8") + jpegData + "\r\n" + \ + ("--%s--" % (boundary)).encode("utf_8") + + + # Modified by Manish for allowing + # upload through proxy + + request = urllib2.Request(url) + request.add_data(postData) + request.add_header("Content-Type", Hdr) + response = urllib2.urlopen(request) + rspXML = response.read() + # Modified by Manish Rai Jain + return XMLNode.parseXML(XMLNode(), xmlStr=rspXML) + except IOError: + return None + + + + #----------------------------------------------------------------------- + #@classmethod + def testFailure(cls, rsp, exit=True): + """Exit app if the rsp XMLNode indicates failure.""" + if rsp['stat'] == "fail": + sys.stderr.write("%s: error %s: %s\n" % (rsp.elementName, \ + rsp.err[0]['code'], rsp.err[0]['msg'])) + if exit: sys.exit(1) + + #----------------------------------------------------------------------- + def __getCachedTokenPath(self): + """Return the directory holding the app data.""" + return os.path.expanduser("~/.flickr/%s" % (self.apiKey)) + + #----------------------------------------------------------------------- + def __getCachedTokenFilename(self): + """Return the full pathname of the cached token file.""" + return "%s/auth.xml" % (self.__getCachedTokenPath()) + + #----------------------------------------------------------------------- + def __getCachedToken(self): + """Read and return a cached token, or None if not found. + + The token is read from the cached token file, which is basically the + entire RSP response containing the auth element. + """ + + try: + f = file(self.__getCachedTokenFilename(), "r") + data = f.read() + f.close() + # Modified here: added XMLNode() as the 1st argument + # Mod by: Manish Rai Jain + rsp = XMLNode.parseXML(XMLNode(), data) + + return rsp.auth[0].token[0].elementText + + except IOError: + return None + + #----------------------------------------------------------------------- + def __setCachedToken(self, xml): + """Cache a token for later use. + + The cached tag is stored by simply saving the entire RSP response + containing the auth element. + + """ + + path = self.__getCachedTokenPath() + if not os.path.exists(path): + os.makedirs(path) + + f = file(self.__getCachedTokenFilename(), "w") + f.write(xml) + f.close() + + + #----------------------------------------------------------------------- + def getToken(self, perms="write", browser="lynx"): + """Get a token either from the cache, or make a new one from the + frob. + + This first attempts to find a token in the user's token cache on + disk. + + If that fails (or if the token is no longer valid based on + flickr.auth.checkToken) a new frob is acquired. The frob is + validated by having the user log into flickr (with lynx), and + subsequently a valid token is retrieved. + + The newly minted token is then cached locally for the next run. + + perms--"read", "write", or "delete" + browser--whatever browser should be used in the system() call + + """ + + # see if we have a saved token + token = self.__getCachedToken() + + # see if it's valid + if token != None: + rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token) + if rsp['stat'] != "ok": + token = None + else: + # see if we have enough permissions + tokenPerms = rsp.auth[0].perms[0].elementText + if tokenPerms == "read" and perms != "read": token = None + elif tokenPerms == "write" and perms == "delete": token = None + + # get a new token if we need one + if token == None: + # get the frob + rsp = self.auth_getFrob(api_key=self.apiKey) + self.testFailure(rsp) + frob = rsp.frob[0].elementText + + # validate online + os.system("%s '%s'" % (browser, self.__getAuthURL(perms, frob))) + # get a token + rsp = self.auth_getToken(api_key=self.apiKey, frob=frob) + self.testFailure(rsp) + token = rsp.auth[0].token[0].elementText + + # store the auth info for next time + self.__setCachedToken(rsp.xml) + + # Modified here: save the auth token to use as a default + # Mod by: R. David Murray + self.token = token + return token + + +######################################################################## +# App functionality +######################################################################## + +def main(argv): + # flickr auth information: + flickrAPIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # API key + flickrSecret = "yyyyyyyyyyyyyyyy" # shared "secret" + + # make a new FlickrAPI instance + fapi = FlickrAPI(flickrAPIKey, flickrSecret) + + # do the whole whatever-it-takes to get a valid token: + token = fapi.getToken(browser="/usr/bin/x-www-browser") + + # get my favorites + rsp = fapi.favorites_getList(api_key=flickrAPIKey,auth_token=token) + fapi.testFailure(rsp) + + # and print them + for a in rsp.photos[0].photo: + print "%10s: %s" % (a['id'], a['title'].encode("ascii", "replace")) + + # upload the file foo.jpg + #rsp = fapi.upload("foo.jpg", api_key=flickrAPIKey, auth_token=token, \ + # title="This is the title", description="This is the description", \ + # tags="tag1 tag2 tag3", is_public="1") + #if rsp == None: + # sys.stderr.write("can't find file\n") + #else: + # fapi.testFailure(rsp) + + return 0 + +# run the main if we're not being imported: +if __name__ == "__main__": sys.exit(main(sys.argv)) + diff -Naur flickrfs-1.3.9.orig/flickrfs/flickrfs.py flickrfs-cvs.orig/flickrfs/flickrfs.py --- flickrfs-1.3.9.orig/flickrfs/flickrfs.py 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/flickrfs/flickrfs.py 2008-10-17 17:20:36.000000000 +0200 @@ -0,0 +1,1108 @@ +#!/usr/bin/python +#=============================================================================== +# flickrfs - Virtual Filesystem for Flickr +# Copyright (c) 2005,2006 Manish Rai Jain +# +# This program can be distributed under the terms of the GNU GPL version 2, or +# its later versions. +# +# DISCLAIMER: The API Key and Shared Secret are provided by the author in +# the hope that it will prevent unnecessary trouble to the end-user. The +# author will not be liable for any misuse of this API Key/Shared Secret +# through this application/derived apps/any 3rd party apps using this key. +#=============================================================================== + +__author__ = "Manish Rai Jain (manishrjain@gmail.com)" +__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" + +import thread, string, ConfigParser, mimetypes, codecs +import time, logging, logging.handlers, os, sys +from glob import glob +from errno import * +from traceback import format_exc +# The python-fuse api has changed in 0.2, but we're still using the 0.1 api. +# The following two lines line will make us compatible with both versions by +# making 2.0 use the 1.0 api. +# (See http://fuse4bsd.creo.hu/README.new_fusepy_api.html) +import fuse +fuse.fuse_python_api = (0, 1) +Fuse = fuse.Fuse +import threading +import random, commands +from urllib2 import URLError +from transactions import TransFlickr +import inodes + +#Some global definitions and functions +NUMRETRIES = 3 +DEFAULTCONFIG = """\ +[configuration] + +browser: /usr/bin/x-www-browser +image.size: +sets.sync.int: 300 +stream.sync.int: 300 +""" + +#Set up the .flickfs directory. +homedir = os.getenv('HOME') +flickrfsHome = os.path.join(homedir, '.flickrfs') +dbPath = os.path.join(flickrfsHome, '.inode.bdb') + +if not os.path.exists(flickrfsHome): + os.mkdir(os.path.join(flickrfsHome)) +else: + # Remove previous metadata files from ~/.flickrfs + for a in glob(os.path.join(flickrfsHome, '.*')): + os.remove(os.path.join(flickrfsHome, a)) + try: + os.remove(dbPath) + except: + pass + +# Added by Varun Hiremath +if not os.path.exists(flickrfsHome + "/config.txt"): + fconfig = open(flickrfsHome+"/config.txt",'w') + fconfig.write(DEFAULTCONFIG) + fconfig.close() + +# Set up logging +rootlogger = logging.getLogger() +loghdlr = logging.handlers.RotatingFileHandler( + os.path.join(flickrfsHome,'log'), "a", 5242880, 3) +logfmt = logging.Formatter("%(asctime)s %(name)-14s %(levelname)-7s %(threadName)-10s %(funcName)-22s %(message)s", "%x %X") +loghdlr.setFormatter(logfmt) +rootlogger.addHandler(loghdlr) +rootlogger.setLevel(logging.DEBUG) +log = logging.getLogger('flickrfs') +logattr = logging.getLogger('flickrfs.attr') +logattr.setLevel(logging.INFO) + +cp = ConfigParser.ConfigParser() +cp.read(flickrfsHome + '/config.txt') +_resizeStr = "" +sets_sync_int = 600.0 +stream_sync_int = 600.0 +try: + _resizeStr = cp.get('configuration', 'image.size') +except: + print 'No default size of image found. Will upload original size of images.' +try: + sets_sync_int = float(cp.get('configuration', 'sets.sync.int')) +except: + pass +try: + stream_sync_int = float(cp.get('configuration', 'stream.sync.int')) +except: + pass +try: + browserName = cp.get('configuration', 'browser') +except: + pass + +# Retrive the resize string. +def GetResizeStr(): + return _resizeStr + +#Utility functions. +def _log_exception_wrapper(func, *args, **kw): + """Call 'func' with args and kws and log any exception it throws. + """ + for i in range(0, NUMRETRIES): + log.debug("retry attempt %s for func %s", i, func.__name__) + try: + func(*args, **kw) + return + except: + log.error("exception in function %s", func.__name__) + log.error(format_exc()) + +def background(func, *args, **kw): + """Run 'func' as a thread, logging any exceptions it throws. + + To run + + somefunc(arg1, arg2='value') + + as a thread, do: + + background(somefunc, arg1, arg2='value') + + Any exceptions thrown are logged as errors, and the traceback is logged. + """ + thread.start_new_thread(_log_exception_wrapper, (func,)+args, kw) + +def timerThread(func, func1, interval): + '''Execute func now, followed by func1 every interval seconds + ''' + log.debug("running first pass funtion %s", func.__name__) + t = threading.Timer(0.0, func) + try: + t.run() + except: + log.debug(format_exc()) + while(interval): + log.debug("scheduling function %s to run in %s seconds", + func1.__name__, interval) + t = threading.Timer(interval, func1) + try: + t.run() + except: + log.debug(format_exc()) + +def retryFlickrOp(isNone, func, *args): + # This function helps in retrying the flickr transactions, in case they fail. + result = None + for i in range(0, NUMRETRIES): + log.debug("retry attempt %d for func %s", i, func.__name__) + try: + result = func(*args) + if result is None: + if isNone: + return result + else: + continue + else: + return result + except URLError, detail: + log.error("URLError in function %s with error: %s", + func.__name__, detail) + # We've utilized all our attempts, send out the result whatever it is. + return result + +class Flickrfs(Fuse): + + def __init__(self, *args, **kw): + + Fuse.__init__(self, *args, **kw) + log.info("mountpoint: %s", repr(self.mountpoint)) + log.info("mount options: %s", ', '.join(self.optlist)) + log.info("named mount options: %s", + ', '.join([ "%s: %s" % (k, v) for k, v in self.optdict.items() ])) + + self.inodeCache = inodes.InodeCache(dbPath) # Inodes need to be stored. + self.imgCache = inodes.ImageCache() + self.NSID = "" + self.transfl = TransFlickr(browserName) + + # Set some variables to be utilized by statfs function. + self.statfsCounter = -1 + self.max = 0L + self.used = 0L + + self.NSID = self.transfl.getUserId() + if self.NSID is None: + log.error("can't retrieve user information") + sys.exit(-1) + + log.info('getting list of licenses available') + self.licenses = self.transfl.getLicenses() + if self.licenses is None: + log.error("can't retrieve license information") + sys.exit(-1) + + # do stuff to set up your filesystem here, if you want + self._mkdir("/") + self._mkdir("/tags") + self._mkdir("/tags/personal") + self._mkdir("/tags/public") + background(timerThread, self.sets_thread, + self.sync_sets_thread, sets_sync_int) #sync every 2 minutes + + + def imageResize(self, bufData): + # If no resizing information is present, then return the buffer directly. + if GetResizeStr() == "": + return bufData + + # Else go ahead and do the conversion. + im = '/tmp/flickrfs-' + str(int(random.random()*1000000000)) + f = open(im, 'w') + f.write(bufData) + f.close() + cmd = 'identify -format "%%w" %s'%(im,) + status,ret = commands.getstatusoutput(cmd) + msg = ("%s command not found; you must install Imagemagick to get " + "auto photo resizing") + if status!=0: + print msg % 'identify' + log.error(msg, identify) + return bufData + try: + if int(ret)0: + log.info('%s photos have been deleted online' % len(psetLocal)) + for c in psetLocal: + log.info('deleting %s', c) + self.unlink("%s/%s" % (curdir, c), False) + + def __sync_set_in_background(self, set_id, curdir): + # Exception handling will be done by background function. + log.info("syncing set %s", curdir) + psetOnline = self.transfl.getPhotosFromPhotoset(set_id) + self._sync_code(psetOnline, curdir) + log.info("set %s sync successfully finished", curdir) + + def sync_sets_thread(self): + log.info("started") + setListOnline = self.transfl.getPhotosetList() + setListLocal = self.getdir('/sets', False) + + for a in setListOnline: + title = a.title[0].elementText.replace('/', '_') + if title.strip()=="": + title = a['id'] + if (title,0) not in setListLocal: #New set added online + log.info("new set %s found online", title) + self._mkdir('/sets/'+title, a['id']) + else: #Present Online + setListLocal.remove((title,0)) + for a in setListLocal: #List of sets present locally, but not online + log.info('set %s no longer online, recursively deleting it', a) + self.rmdir('/sets/'+a[0], online=False, recr=True) + + for a in setListOnline: + title = a.title[0].elementText.replace('/', '_') + curdir = "/sets/" + title + if title.strip()=='': + curdir = "/sets/" + a['id'] + set_id = a['id'] + self.__sync_set_in_background(set_id, curdir) + log.info('finished') + + def sync_stream_thread(self): + log.info('started') + psetOnline = self.transfl.getPhotoStream(self.NSID) + self._sync_code(psetOnline, '/stream') + log.info('finished') + + def stream_thread(self): + log.info("started") + print "Populating photostream" + for b in self.transfl.getPhotoStream(self.NSID): + info = self.transfl.parseInfoFromPhoto(b) + self._mkfileWithMeta('/stream', info) + log.info("finished") + print "Photostream population finished." + + def tags_thread(self, path): + ind = string.rindex(path, '/') + tagName = path[ind+1:] + if tagName.strip()=='': + log.error("the tagName '%s' doesn't contain any tags", tagName) + return + log.info("started for %s", tagName) + sendtagList = ','.join(tagName.split(':')) + if(path.startswith('/tags/personal')): + user_id = self.NSID + else: + user_id = None + for b in self.transfl.getTaggedPhotos(sendtagList, user_id): + info = self.transfl.parseInfoFromPhoto(b) + self._mkfileWithMeta(path, info) + + def getUnixPerms(self, info): + mode = info.get('mode') + if mode is not None: + return mode + perms = info.get('perms') + if perms is None: + return 0644 + if perms is "1": # public + return 0755 + elif perms is "2": # friends only. Add 1 to 4 in middle letter. + return 0754 + elif perms is "3": # family only. Add 2 to 4 in middle letter. + return 0764 + elif perms is "4": # friends and family. Add 1+2 to 4 in middle letter. + return 0774 + else: + return 0744 # private + + def __getImageTitle(self, title, id, format = "jpg"): + temp = title.replace('/', '') +# return "%s_%s.%s" % (temp[:32], id, format) + # Store the photos original name. Thus, when pictures are uploaded + # their names would remain as it is, allowing easy resumption of + # uploading of images, in case some of the photos fail uploading. + return "%s.%s" % (temp, format) + + def _mkfileWithMeta(self, path, info): + # Don't write the meta information here, because it requires access to + # the full INFO. Only do with the smaller version of information that + # is provided. + if info is None: + return + title = info.get("title", "") + id = info.get("id", "") + ext = info.get("format", "jpg") + title = self.__getImageTitle(title, id, ext) + + # Refactor this section of code, so that it can be called + # from read. + # Figure out a way to retrieve information, which can be + # used in _mkfile. + mtime = info.get("dupdate") + ctime = info.get("dupload") + perms = self.getUnixPerms(info) + self._mkfile(path+"/"+title, id=id, mode=perms, mtime=mtime, ctime=ctime) + self._mkfile(path+'/.'+title+'.meta', id) + + def _parsepathid(self, path, id=""): + #Path and Id may be unicode strings, so encode them to utf8 now before + #we use them, otherwise python will throw errors when we combine them + #with regular strings. + path = path.encode('utf8') + if id!=0: id = id.encode('utf8') + parentDir, name = os.path.split(path) + if parentDir=='': + parentDir = '/' + log.debug("parentDir %s", parentDir) + return path, id, parentDir, name + + def _mkdir(self, path, id="", mtime=None, ctime=None): + path, id, parentDir, name = self._parsepathid(path, id) + log.debug("creating directory %s", path) + self.inodeCache[path] = inodes.DirInode(path, id, mtime=mtime, ctime=ctime) + if path!='/': + pinode = self.getInode(parentDir) + pinode.nlink += 1 + self.updateInode(parentDir, pinode) + log.debug("nlink of %s is now %s", parentDir, pinode.nlink) + + def _mkfile(self, path, id="", mode=None, + comm_meta="", mtime=None, ctime=None): + path, id, parentDir, name = self._parsepathid(path, id) + log.debug("creating file %s with id %s", path, id) + image_name, extension = os.path.splitext(name) + if not extension: + log.error("can't create file without extension") + return + fInode = inodes.FileInode(path, id, mode=mode, comm_meta=comm_meta, + mtime=mtime, ctime=ctime) + self.inodeCache[path] = fInode + # Now create the meta info inode if the meta info file exists + # refactoring: create the meta info inode, regardless of the + # existence of datapath. +# path = os.path.join(parentDir, '.' + image_name + '.meta') +# datapath = os.path.join(flickrfsHome, '.'+id) +# if os.path.exists(datapath): +# size = os.path.getsize(datapath) +# self.inodeCache[path] = FileInode(path, id) + + def getattr(self, path): + # getattr is being called 4-6 times every second for '/' + # Don't log those calls, as they clutter up the log file. + if path != "/": + logattr.debug("getattr: %s", path) + templist = path.split('/') + if path.startswith('/sets/'): + templist[2] = templist[2].split(':')[0] + elif path.startswith('/stream'): + templist[1] = templist[1].split(':')[0] + path = '/'.join(templist) + + inode=self.getInode(path) + if inode: + #log.debug("inode %s", inode) + statTuple = (inode.mode,inode.ino,inode.dev,inode.nlink, + inode.uid,inode.gid,inode.size,inode.atime,inode.mtime,inode.ctime) + #log.debug("statsTuple %s", statTuple) + return statTuple + else: + e = OSError("No such file"+path) + e.errno = ENOENT + raise e + + def readlink(self, path): + log.debug("readlink") + return os.readlink(path) + + def getdir(self, path, hidden=True): + logattr.debug("getdir: %s", path) + templist = [] + if hidden: + templist = ['.', '..'] + for a in self.inodeCache.keys(): + ind = a.rindex('/') + if path=='/': + path="" + if path==a[:ind]: + name = a.split('/')[-1] + if name=="": + continue + if hidden and name.startswith('.'): + templist.append(name) + elif not name.startswith('.'): + templist.append(name) + return map(lambda x: (x,0), templist) + + def unlink(self, path, online=True): + log.debug("unlink %s", path) + if self.inodeCache.has_key(path): + inode = self.inodeCache.pop(path) + # Remove the meta data file as well if it exists + if self.inodeCache.has_key(path + ".meta"): + self.inodeCache.pop(path + ".meta") + + typesinfo = mimetypes.guess_type(path) + if typesinfo[0] is None or typesinfo[0].count('image')<=0: + log.debug("unlinked non-image file %s", path) + return + + if path.startswith('/sets/'): + ind = path.rindex('/') + pPath = path[:ind] + pinode = self.getInode(pPath) + if online: + self.transfl.removePhotofromSet(photoId=inode.photoId, + photosetId=pinode.setId) + log.info("photo %s removed from set", path) + del inode + else: + log.error("%s is not a known file", path) + #Dont' raise an exception. Not useful when + #using editors like Vim. They make loads of + #crap buffer files + + def rmdir(self, path, online=True, recr=False): + log.debug("removing %s", path) + if self.inodeCache.has_key(path): + for a in self.inodeCache.keys(): + if a.startswith(path+'/'): + if recr: + self.unlink(a, online) + else: + e = OSError("Directory not empty") + e.errno = ENOTEMPTY + raise e + else: + log.error("%s is not a known directory", path) + e = OSError("No such folder"+path) + e.errno = ENOENT + raise e + + if path=='/sets' or path=='/tags' or path=='/tags/personal' \ + or path=='/tags/public' or path=='/stream': + log.debug("attempt to remove framework file %s rejected", path) + e = OSError("removal of folder %s not allowed" % (path)) + e.errno = EPERM + raise e + + ind = path.rindex('/') + pPath = path[:ind] + inode = self.inodeCache.pop(path) + if online and path.startswith('/sets/'): + self.transfl.deleteSet(inode.setId) + del inode + pInode = self.getInode(pPath) + pInode.nlink -= 1 + self.updateInode(pPath, pInode) + + def symlink(self, path, path1): + log.debug("symlink") + return os.symlink(path, path1) + + def rename(self, path, path1): + log.debug("%s %s", path, path1) + #Donot allow Vim to create a file~ + #Check for .meta in both paths + if path.count('~')>0 or path1.count('~')>0: + log.debug("vim enablement path entered") + try: + #Get inode, but _dont_ remove from cache + inode = self.getInode(path) + if inode is not None: + self.inodeCache[path1] = inode + except: + log.debug("couldn't find inode for %s", path) + return + + #Read from path + inode = self.getInode(path) + if inode is None or not hasattr(inode, 'photoId'): + return + fname = os.path.join(flickrfsHome, '.'+inode.photoId) + f = open(fname, 'r') + buf = f.read() + f.close() + + #Now write to path1 + inode = self.getInode(path1) + if inode is None or not hasattr(inode, 'photoId'): + return + fname = os.path.join(flickrfsHome, '.'+inode.photoId) + f = open(fname, 'w') + f.write(buf) + f.close() + inode.size = os.path.getsize(fname) + self.updateInode(path1, inode) + retinfo = self.parse(fname, inode.photoId) + if retinfo.count('Error')>0: + log.error(retinfo) + + def link(self, srcpath, destpath): + log.debug("%s %s", srcpath, destpath) + #Add image from stream to set, w/o retrieving + slist = srcpath.split('/') + sname_file = slist.pop(-1) + dlist = destpath.split('/') + dname_file = dlist.pop(-1) + error = 0 + if sname_file=="" or sname_file.startswith('.'): + error = 1 + if dname_file=="" or dname_file.startswith('.'): + error = 1 + if not destpath.startswith('/sets/'): + error = 1 + if error is 1: + log.error("linking is allowed only between 2 image files") + return + sinode = self.getInode(srcpath) + self._mkfile(destpath, id=sinode.id, mode=sinode.mode, + comm_meta=sinode.comm_meta, mtime=sinode.mtime, + ctime=sinode.ctime) + parentPath = '/'.join(dlist) + pinode = self.getInode(parentPath) + if pinode.setId==0: + try: + pinode.setId = self.transfl.createSet(parentPath, sinode.photoId) + self.updateInode(parentPath, pinode) + except: + e = OSError("Can't create a new set") + e.errno = EIO + raise e + else: + self.transfl.put2Set(pinode.setId, sinode.photoId) + + + def chmod(self, path, mode): + log.debug("%s %s" % path, mode) + inode = self.getInode(path) + typesinfo = mimetypes.guess_type(path) + + if inode.comm_meta is None: + log.debug("chmod on directory ignored") + return + + elif typesinfo[0] is None or typesinfo[0].count('image')<=0: + + os.chmod(path, mode) + return + + elif self.transfl.setPerm(inode.photoId, mode, inode.comm_meta)==True: + inode.mode = mode + self.updateInode(path, inode) + return + + def chown(self, path, user, group): + log.debug("%s:%s %s (ignored)", user, group, path) + + def truncate(self, path, size): + log.debug("%s %s", path, size) + ind = path.rindex('/') + name_file = path[ind+1:] + + typeinfo = mimetypes.guess_type(path) + if typeinfo[0] is None or typeinfo[0].count('image')<=0: + inode = self.getInode(path) + filePath = os.path.join(flickrfsHome, '.'+inode.photoId) + f = open(filePath, 'w+') + return f.truncate(size) + + def mknod(self, path, mode, dev): + log.debug("%s %s %s ", path, mode, dev) + templist = path.split('/') + name_file = templist[-1] + + if name_file.startswith('.') and name_file.count('.meta') > 0: + # We need to handle the special case, where some meta files are being + # created through mknod. Creation of meta files is done when adding + # images automatically; and they should not go through mknod system call. + # Editors like Vim, try to generate random swap files when reading + # meta; and this should be *disallowed*. + log.debug("mknod for meta file %s ignored", path) + return + + if path.startswith('/sets/'): + templist[2] = templist[2].split(':')[0] + elif path.startswith('/stream'): + templist[1] = templist[1].split(':')[0] + path = '/'.join(templist) + + log.debug("modified file path %s", path) + #Lets guess what kind of a file is this. + #Is it an image file? or, some other temporary file + #created by the tools you're using. + typeinfo = mimetypes.guess_type(path) + if typeinfo[0] is None or typeinfo[0].count('image') <= 0: + f = open(os.path.join(flickrfsHome,'.'+name_file), 'w') + f.close() + # TODO(manishrjain): This should not be FileInode, it should rather be + # Inode. + self.inodeCache[path] = inodes.FileInode(path, name_file, mode=mode) + else: + self._mkfile(path, id="NEW", mode=mode) + + def mkdir(self, path, mode): + log.debug("%s with mode %s", path, mode) + if path.startswith("/tags"): + if path.count('/')==3: #/tags/personal (or private)/dirname ONLY + self._mkdir(path) + background(self.tags_thread, path) + else: + e = OSError("Not allowed to create directory %s" % path) + e.errno = EACCES + raise e + elif path.startswith("/sets"): + if path.count('/')==2: #Only allow creation of new set /sets/newset + self._mkdir(path, id=0) + #id=0 means that not yet created online + else: + e = OSError("Not allowed to create directory %s" % path) + e.errno = EACCES + raise e + elif path=='/stream': + self._mkdir(path) + background(timerThread, self.stream_thread, + self.sync_stream_thread, stream_sync_int) + + else: + e = OSError("Not allowed to create directory %s" % path) + e.errno = EACCES + raise e + + def utime(self, path, times): + inode = self.getInode(path) + inode.atime = times[0] + inode.mtime = times[1] + self.updateInode(path, inode) + return 0 + + def open(self, path, flags): + log.info("%s with flags %s", path, flags) + ind = path.rindex('/') + name_file = path[ind+1:] + if name_file.startswith('.') and name_file.endswith('.meta'): + self.handleAccessToNonImage(path) + return 0 + typesinfo = mimetypes.guess_type(path) + if typesinfo[0] is None or typesinfo[0].count('image')<=0: + log.debug('non-image file found %s', path) + self.handleAccessToNonImage(path) + return 0 + + templist = path.split('/') + if path.startswith('/sets/'): + templist[2] = templist[2].split(':')[0] + elif path.startswith('/stream'): + templist[1] = templist[1].split(':')[0] + path = '/'.join(templist) + log.debug("path after modification is %s", path) + + inode = self.getInode(path) + if inode.photoId=="NEW": #Just skip if new (i.e. uploading) + return 0 + if self.imgCache.getBuffer(inode.photoId)=="": + log.debug("retrieving image %s from flickr", inode.photoId) + self.imgCache.setBuffer(inode.photoId, + str(self.transfl.getPhoto(inode.photoId))) + inode.size = self.imgCache.getBufLen(inode.photoId) + log.debug("size of image is %s", inode.size) + self.updateInode(path, inode) + return 0 + + def read(self, path, length, offset): + log.debug("%s offset %s length %s", path, offset, length) + ind = path.rindex('/') + name_file = path[ind+1:] + if name_file.startswith('.') and name_file.endswith('.meta'): + # Check if file is not present. If not, retrieve and + # create the file locally. + buf = self.handleReadNonImage(path, length, offset) + return buf + typesinfo = mimetypes.guess_type(path) + if typesinfo[0] is None or typesinfo[0].count('image')<=0: + return self.handleReadNonImage(path, length, offset) + return self.handleReadImage(path, length, offset) + + def parse(self, fname, photoId): + cp = ConfigParser.ConfigParser() + log.debug("parsing file %s for photoid %s", fname, photoId) + cp.read(fname) + log.debug("file %s has been read by ConfigParser", fname) + options = cp.options('metainfo') + title='' + desc='' + tags='' + license='' + if 'description' in options: + desc = cp.get('metainfo', 'description') + if 'tags' in options: + tags = cp.get('metainfo', 'tags') + if 'title' in options: + title = cp.get('metainfo', 'title') + if 'license' in options: + license = cp.get('metainfo', 'license') + + log.debug("setting metadata for file %s", fname) + if self.transfl.setMeta(photoId, title, desc)==False: + return "Error:Can't set Meta information" + + log.debug("setting tags for %s", fname) + if self.transfl.setTags(photoId, tags)==False: + log.debug("setting tags for %s failed", fname) + return "Error:Can't set tags" + + log.debug("setting license for %s", fname) + if self.transfl.setLicense(photoId, license)==False: + return "Error:Can't set license" + + # except: + # log.error("Can't parse file:%s:"%(fname,)) + # return "Error:Can't parse" + return 'Success:Updated photo:%s:%s:'%(fname,photoId) + + ################################################## + # 'handle' Functions for handling read and writes. + ################################################## + def handleAccessToNonImage(self, path): + inode = self.getInode(path) + if inode is None: + log.error("inode %s doesn't exist", path) + e = OSError("No inode found") + e.errno = EIO + raise e + fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext + # Handle the case when file already exists. + if not os.path.exists(fname) or os.path.getsize(fname) == 0L: + log.info("retrieving meta information for file %s and photo id %s", + fname, inode.photoId) + INFO = self.transfl.getPhotoInfo(inode.photoId) + size = self.writeMetaInfo(inode.photoId, INFO) + log.info("information has been written for photo id %s", inode.photoId) + inode.size = size + self.updateInode(path, inode) + time.sleep(1) # Enough time for OS to call for getattr again. + return inode + + def handleReadNonImage(self, path, length, offset): + inode = self.handleAccessToNonImage(path) + f = open(os.path.join(flickrfsHome, '.'+inode.photoId), 'r') + f.seek(offset) + return f.read(length) + + def handleReadImage(self, path, length, offset): + inode = self.getInode(path) + if inode is None: + log.error("inode %s doesn't exist", path) + e = OSError("No inode found") + e.errno = EIO + raise e + if self.imgCache.getBufLen(inode.photoId) is 0: + log.debug("retrieving image %s from flickr", inode.photoId) + buf = retryFlickrOp(False, self.transfl.getPhoto, + inode.photoId) + if len(buf) == 0: + log.error("can't retrieve image %s", inode.photoId) + e = OSError("Unable to retrieve image.") + e.errno = EIO + raise e + self.imgCache.setBuffer(inode.photoId, buf) + inode.size = self.imgCache.getBufLen(inode.photoId) + temp = self.imgCache.getBuffer(inode.photoId, offset, offset+length) + if len(temp) < length: + self.imgCache.popBuffer(inode.photoId) + self.updateInode(path, inode) + return temp + + def handleWriteToNonImage(self, path, buf, off): + inode = self.handleAccessToNonImage(path) + fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext + log.debug("writing to %s", fname) + f = open(fname, 'r+') + f.seek(off) + f.write(buf) + f.close() + if len(buf)<4096: + inode.size = os.path.getsize(fname) + retinfo = self.parse(fname, inode.photoId) + if retinfo.count('Error')>0: + e = OSError(retinfo.split(':')[1]) + e.errno = EIO + raise e + self.updateInode(path, inode) + return len(buf) + + def handleUploadingImage(self, path, inode, taglist): + tags = [ '"%s"'%(a,) for a in taglist] + tags.append('flickrfs') + taglist = ' '.join(tags) + log.info('uploading %s with len %s', + path, self.imgCache.getBufLen(inode.photoId)) + id = None + bufData = self.imgCache.getBuffer(inode.photoId) + bufData = self.imageResize(bufData) + id = retryFlickrOp(False, self.transfl.uploadfile, + path, taglist, bufData, inode.mode) + if id is None: + log.error("unable to upload file %s", inode.photoId) + e = OSError("Unable to upload file.") + e.errno = EIO + raise e + self.imgCache.popBuffer(inode.photoId) + inode.photoId = id + self.updateInode(path, inode) + return inode + + def handleWriteToBuffer(self, path, buf): + inode = self.getInode(path) + if inode is None: + log.error("inode %s doesn't exist", path) + e = OSError("No inode found") + e.errno = EIO + raise e + self.imgCache.addBuffer(inode.photoId, buf) + return inode + + def handleWriteAddToSet(self, parentPath, pinode, inode): + #Create set if it doesn't exist online (i.e. if id=0) + if pinode.setId is 0: + # Retry creation of set if unsuccessful. + pinode.setId = retryFlickrOp(False, self.transfl.createSet, + parentPath, inode.photoId) + # If the set is created, then return. + if pinode.setId is not None: + self.updateInode(parentPath, pinode) + return + else: + log.error("unable to create set %s", parentPath) + e = OSError("Unable to create set.") + e.errno = EIO + raise e + else: + # If the operation put2Set doesn't throw exception, that means + # that the picture has been successfully added to set. + # Return in that case, retry otherwise. + retryFlickrOp(True, self.transfl.put2Set, + pinode.setId, inode.photoId) + return + + ############################# + # End of 'handle' Functions. + ############################# + + def write(self, path, buf, off): + log.debug("write to %s at offset %s", path, off) + ind = path.rindex('/') + name_file = path[ind+1:] + if name_file.startswith('.') and name_file.count('.meta')>0: + return self.handleWriteToNonImage(path, buf, off) + typesinfo = mimetypes.guess_type(path) + if typesinfo[0] is None or typesinfo[0].count('image')<=0: + return self.handleWriteToNonImage(path, buf, off) + templist = path.split('/') + inode = None + if path.startswith('/tags'): + e = OSError("Copying to tags not allowed") + e.errno = EIO + raise e + if path.startswith('/stream'): + tags = templist[1].split(':') + templist[1] = tags.pop(0) + path = '/'.join(templist) + inode = self.handleWriteToBuffer(path, buf) + if len(buf) < 4096: + self.handleUploadingImage(path, inode, tags) + elif path.startswith('/sets/'): + setnTags = templist[2].split(':') + setName = setnTags.pop(0) + templist[2] = setName + path = '/'.join(templist) + inode = self.handleWriteToBuffer(path, buf) + if len(buf) < 4096: + templist.pop(-1) + parentPath = '/'.join(templist) + pinode = self.getInode(parentPath) + inode = self.handleUploadingImage(path, inode, setnTags) + self.handleWriteAddToSet(parentPath, pinode, inode) + log.debug("done write to %s at offset %s", path, off) + if len(buf)<4096: + templist = path.split('/') + templist.pop(-1) + parentPath = '/'.join(templist) + try: + self.inodeCache.pop(path) + except: + pass + INFO = self.transfl.getPhotoInfo(inode.photoId) + info = self.transfl.parseInfoFromFullInfo(inode.photoId, INFO) + self._mkfileWithMeta(parentPath, info) + self.writeMetaInfo(inode.photoId, INFO) + return len(buf) + + def getInode(self, path): + if self.inodeCache.has_key(path): + #log.debug("got cached inode for %s", path) + return self.inodeCache[path] + else: + #log.debug("%s is not in inode cache", path) + return None + + def updateInode(self, path, inode): + self.inodeCache[path] = inode + + def release(self, path, flags): + log.debug("%s with flags %s ignored", path, flags) + return 0 + + def statfs(self): + """ + Should return a tuple with the following elements in respective order: + + F_BSIZE - Preferred file system block size. (int) + F_FRSIZE - Fundamental file system block size. (int) + F_BLOCKS - Total number of blocks in the filesystem. (long) + F_BFREE - Total number of free blocks. (long) + F_BAVAIL - Free blocks available to non-super user. (long) + F_FILES - Total number of file nodes. (long) + F_FFREE - Total number of free file nodes. (long) + F_FAVAIL - Free nodes available to non-super user. (long) + F_FLAG - Flags. System dependent: see statvfs() man page. (int) + F_NAMEMAX - Maximum file name length. (int) + Feel free to set any of the above values to 0, which tells + the kernel that the info is not available. + """ + block_size = 1024 + blocks = 0L + blocks_free = 0L + files = 0L + files_free = 0L + namelen = 255 + # statfs is being called repeatedly at least once a second. + # The bandwidth information doesn't change that often, so + # save upon communication with flickr servers to retrieve this + # information. Only retrieve it once in a while. + if self.statfsCounter >= 500 or self.statfsCounter is -1: + (self.max, self.used) = self.transfl.getBandwidthInfo() + self.statfsCounter = 0 + log.info('retrieved bandwidth info: max %s used %s', + self.max, self.used) + self.statfsCounter = self.statfsCounter + 1 + + if self.max is not None: + blocks = long(self.max)/block_size + blocks_used = long(self.used)/block_size + blocks_free = blocks - blocks_used + blocks_available = blocks_free + return (block_size, blocks, blocks_free, blocks_available, + files, files_free, namelen) + + def fsync(self, path, isfsyncfile): + log.debug("%s, isfsyncfile=%s", path, isfsyncfile) + return 0 + + +if __name__ == '__main__': + try: + server = Flickrfs() + server.multithreaded = 1; + server.main() + except KeyError: + log.error('got key error; exiting...') + sys.exit(0) diff -Naur flickrfs-1.3.9.orig/flickrfs/inodes.py flickrfs-cvs.orig/flickrfs/inodes.py --- flickrfs-1.3.9.orig/flickrfs/inodes.py 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/flickrfs/inodes.py 2007-03-29 16:44:26.000000000 +0200 @@ -0,0 +1,134 @@ +#=============================================================================== +# flickrfs - Virtual Filesystem for Flickr +# Copyright (c) 2005,2006 Manish Rai Jain +# +# This program can be distributed under the terms of the GNU GPL version 2, or +# its later versions. +# +# DISCLAIMER: The API Key and Shared Secret are provided by the author in +# the hope that it will prevent unnecessary trouble to the end-user. The +# author will not be liable for any misuse of this API Key/Shared Secret +# through this application/derived apps/any 3rd party apps using this key. +#=============================================================================== + +__author__ = "Manish Rai Jain (manishrjain@gmail.com)" +__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" + +import os, sys, time +from stat import * +import cPickle + +DEFAULTBLOCKSIZE = 4*1024 # 4 KB + +class Inode(object): + """Common base class for all file system objects + """ + def __init__(self, path=None, id='', mode=None, + size=0L, mtime=None, ctime=None): + self.nlink = 1 + self.size = size + self.id = id + self.mode = mode + self.ino = long(time.time()) + self.dev = 409089L + self.uid = int(os.getuid()) + self.gid = int(os.getgid()) + now = int(time.time()) + self.atime = now + if mtime is None: + self.mtime = now + else: + self.mtime = int(mtime) + if ctime is None: + self.ctime = now + else: + self.ctime = int(ctime) + self.blocksize = DEFAULTBLOCKSIZE + +class DirInode(Inode): + def __init__(self, path=None, id="", mode=None, mtime=None, ctime=None): + if mode is None: mode = 0755 + super(DirInode, self).__init__(path, id, mode, 0L, mtime, ctime) + self.mode = S_IFDIR | self.mode + self.nlink += 1 + self.dirfile = "" + self.setId = self.id + + +class FileInode(Inode): + def __init__(self, path=None, id="", mode=None, comm_meta="", + size=0L, mtime=None, ctime=None): + if mode is None: mode = 0644 + super(FileInode, self).__init__(path, id, mode, size, mtime, ctime) + self.mode = S_IFREG | self.mode + self.photoId = self.id + self.comm_meta = comm_meta + + +class ImageCache: + def __init__(self): + self.bufDict = {} + + def setBuffer(self, id, buf): + self.bufDict[id] = buf + + def addBuffer(self, id, inc): + buf = self.getBuffer(id) + self.setBuffer(id, buf+inc) + + def getBuffer(self, id, start=0, end=0): + if end == 0: + return self.bufDict.get(id, "")[start:] + else: + return self.bufDict.get(id, "")[start:end] + + def getBufLen(self, id): + return long(len(self.bufDict.get(id, ""))) + + def popBuffer(self, id): + if id in self.bufDict: + return self.bufDict.pop(id) + + +class InodeCache(dict): + def __init__(self, dbPath): + dict.__init__(self) + try: + import bsddb + # If bsddb is available, utilize that package + # and store the inodes in database. + self.db = bsddb.btopen(dbPath, flag='c') + except: + # Otherwise, store the inodes in memory. + self.db = {} + # Keep the keys in memory. + self.keysCache = set() + + def __getitem__(self, key, d=None): + # key k may be unicode, so convert it to + # a normal string first. + if not self.has_key(key): + return d + valObjStr = self.db.get(str(key)) + return cPickle.loads(valObjStr) + + def __setitem__(self, key, value): + self.keysCache.add(key) + self.db[str(key)] = cPickle.dumps(value) + + def get(self, k, d=None): + return self.__getitem__(k, d) + + def keys(self): + return list(self.keysCache) + + def pop(self, k, *args): + # key k may be unicode, so convert it to + # a normal string first. + valObjStr = self.db.pop(str(k), *args) + self.keysCache.discard(k) + if valObjStr != None: + return cPickle.loads(valObjStr) + + def has_key(self, k): + return k in self.keysCache diff -Naur flickrfs-1.3.9.orig/flickrfs/transactions.py flickrfs-cvs.orig/flickrfs/transactions.py --- flickrfs-1.3.9.orig/flickrfs/transactions.py 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/flickrfs/transactions.py 2008-01-31 20:32:34.000000000 +0100 @@ -0,0 +1,443 @@ +#=============================================================================== +# flickrfs - Virtual Filesystem for Flickr +# Copyright (c) 2005,2006 Manish Rai Jain +# +# This program can be distributed under the terms of the GNU GPL version 2, or +# its later versions. +# +# DISCLAIMER: The API Key and Shared Secret are provided by the author in +# the hope that it will prevent unnecessary trouble to the end-user. The +# author will not be liable for any misuse of this API Key/Shared Secret +# through this application/derived apps/any 3rd party apps using this key. +#=============================================================================== + +__author__ = "Manish Rai Jain (manishrjain@gmail.com)" +__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" + +from flickrapi import FlickrAPI +from traceback import format_exc +import urllib2 +import sys +import string +import os +import time +import logging +log = logging.getLogger('flickrfs.trans') + +# flickr auth information +flickrAPIKey = "f8aa9917a9ae5e44a87cae657924f42d" # API key +flickrSecret = "3fbf7144be7eca28" # shared "secret" + +# Utility functions +def kwdict(**kw): return kw + +#Transactions with flickr, wraps FlickrAPI +# calls in Flickfs-specialized functions. +class TransFlickr: + + extras = "original_format,date_upload,last_update" + + def __init__(self, browserName): + self.fapi = FlickrAPI(flickrAPIKey, flickrSecret) + self.user_id = "" + # proceed with auth + # TODO use auth.checkToken function if available, + # and wait after opening browser. + print "Authorizing with flickr..." + log.info("authorizing with flickr...") + try: + self.authtoken = self.fapi.getToken(browser=browserName) + except: + print ("Can't retrieve token from browser %s" % browserName) + print ("\tIf you're behind a proxy server," + " first set http_proxy environment variable.") + print "\tPlease close all your browser windows, and try again" + log.error(format_exc()) + log.error("can't retrieve token from browser %s", browserName) + sys.exit(-1) + if self.authtoken == None: + print "Unable to authorize (reason unknown)" + log.error('not able to authorize; exiting') + sys.exit(-1) + #Add some authorization checks here(?) + print "Authorization complete." + log.info('authorization complete') + + def uploadfile(self, filepath, taglist, bufData, mode): + #Set public 4(always), 1(public). Public overwrites f&f. + public = mode&1 + #Set friends and family 4(always), 2(family), 1(friends). + friends = mode>>3 & 1 + family = mode>>4 & 1 + #E.g. 745 - 4:No f&f, but 5:public + #E.g. 754 - 5:friends, but not public + #E.g. 774 - 7:f&f, but not public + + log.info("uploading file %s", filepath) + log.info(" data length: %s", len(bufData)) + log.info(" taglist: %s", taglist) + log.info(" permissions: family %s, friends %s, public %s", + family, friends, public) + filename = os.path.splitext(os.path.basename(filepath))[0] + rsp = self.fapi.upload(filename=filepath, jpegData=bufData, + title=filename, + tags=taglist, + is_public=public and "1" or "0", + is_friend=friends and "1" or "0", + is_family=family and "1" or "0") + + if rsp is None: + log.error("response None from attempt to write file %s", filepath) + log.error("will attempt recovery...") + recent_rsp = None + trytimes = 2 + while(trytimes): + log.info("sleeping for 3 seconds...") + time.sleep(3) + trytimes -= 1 + # Keep on trying to retrieve the recently uploaded photo, till we + # actually get the information, or the function throws an exception. + while(recent_rsp is None or not recent_rsp): + recent_rsp = self.fapi.photos_recentlyUpdated( + auth_token=self.authtoken, min_date='1', per_page='1') + + pic = recent_rsp.photos[0].photo[0] + log.info('we are looking for %s', filename) + log.info('most recently updated pic is %s', pic['title']) + if filename == pic['title']: + id = pic['id'] + log.info("file %s uploaded with photoid %s", filepath, id) + return id + log.error("giving up; upload of %s appears to have failed", filepath) + return None + else: + id = rsp.photoid[0].elementText + log.info("file %s uploaded with photoid %s", filepath, id) + return id + + def put2Set(self, set_id, photo_id): + log.info("uploading photo %s to set id %s", photo_id, set_id) + rsp = self.fapi.photosets_addPhoto(auth_token=self.authtoken, + photoset_id=set_id, photo_id=photo_id) + if rsp: + log.info("photo uploaded to set") + else: + log.error(rsp.errormsg) + + def createSet(self, path, photo_id): + log.info("creating set %s with primary photo %s", path, photo_id) + path, title = os.path.split(path) + rsp = self.fapi.photosets_create(auth_token=self.authtoken, + title=title, primary_photo_id=photo_id) + if rsp: + log.info("created set %s", title) + return rsp.photoset[0]['id'] + else: + log.error(rsp.errormsg) + + def deleteSet(self, set_id): + log.info("deleting set %s", set_id) + if str(set_id)=="0": + log.info("ignoring attempt to delete set wtih set_id 0 (a locally " + "created set that has not yet acquired an id via uploading") + return + rsp = self.fapi.photosets_delete(auth_token=self.authtoken, + photoset_id=set_id) + if rsp: + log.info("deleted set %s", set_id) + else: + log.error(rsp.errormsg) + + def getPhotoInfo(self, photoId): + log.debug("id: %s", photoId) + rsp = self.fapi.photos_getInfo(auth_token=self.authtoken, photo_id=photoId) + if not rsp: + log.error("can't retrieve information about photo %s; got error %s", + photoId, rsp.errormsg) + return None + #XXX: should see if there's some other 'format' option we can fall back to. + try: format = rsp.photo[0]['originalformat'] + except KeyError: format = 'jpg' + perm_public = rsp.photo[0].visibility[0]['ispublic'] + perm_family = rsp.photo[0].visibility[0]['isfamily'] + perm_friend = rsp.photo[0].visibility[0]['isfriend'] + if perm_public == '1': + mode = 0755 + else: + b_cnt = 4 + if perm_family == '1': + b_cnt += 2 + if perm_friend == '1': + b_cnt += 1 + mode = "07" + str(b_cnt) + "4" + mode = int(mode) + + if hasattr(rsp.photo[0],'permissions'): + permcomment = rsp.photo[0].permissions[0]['permcomment'] + permaddmeta = rsp.photo[0].permissions[0]['permaddmeta'] + else: + permcomment = permaddmeta = [None] + + commMeta = '%s%s' % (permcomment,permaddmeta) # Required for chmod. + desc = rsp.photo[0].description[0].elementText + title = rsp.photo[0].title[0].elementText + if hasattr(rsp.photo[0].tags[0], "tag"): + taglist = [ a.elementText for a in rsp.photo[0].tags[0].tag ] + else: + taglist = [] + license = rsp.photo[0]['license'] + owner = rsp.photo[0].owner[0]['username'] + ownerNSID = rsp.photo[0].owner[0]['nsid'] + url = rsp.photo[0].urls[0].url[0].elementText + posted = rsp.photo[0].dates[0]['posted'] + lastupdate = rsp.photo[0].dates[0]['lastupdate'] + return (format, mode, commMeta, desc, title, taglist, + license, owner, ownerNSID, url, int(posted), int(lastupdate)) + + def setPerm(self, photoId, mode, comm_meta="33"): + log.debug("id: %s, mode: %s, comm_meta=%s", photoId, mode, comm_meta) + public = mode&1 #Set public 4(always), 1(public). Public overwrites f&f + #Set friends and family 4(always), 2(family), 1(friends) + friends = mode>>3 & 1 + family = mode>>4 & 1 + if len(comm_meta)<2: + # This wd patch string index out of range bug, caused + # because some photos may not have comm_meta value set. + comm_meta="33" + rsp = self.fapi.photos_setPerms(auth_token=self.authtoken, + is_public=str(public), + is_friend=str(friends), + is_family=str(family), + perm_comment=comm_meta[0], + perm_addmeta=comm_meta[1], + photo_id=photoId) + if not rsp: + log.error("couldn't set permission for photo %s; got error %s", + photoId, rsp.errormsg) + return False + log.info("permissions have been set for photo %s", photoId) + return True + + def setTags(self, photoId, tags): + log.debug("id: %s, tags: %s", photoId, tags) + templist = [ '"%s"'%(a,) for a in string.split(tags, ',')] + ['flickrfs'] + tagstring = ' '.join(templist) + rsp = self.fapi.photos_setTags(auth_token=self.authtoken, + photo_id=photoId, tags=tagstring) + if not rsp: + log.error("couldn't set tags for %s; got error %s", + photoId, rsp.errormsg) + return False + return True + + def setMeta(self, photoId, title, desc): + log.debug("id: %s, title: %s, desc: %s", photoId, title, desc) + rsp = self.fapi.photos_setMeta(auth_token=self.authtoken, + photo_id=photoId, title=title, + description=desc) + if not rsp: + log.error("couldn't set meta info for photo %s; got error", + photoId, rsp.errormsg) + return False + return True + + def getLicenses(self): + log.debug("started") + rsp = self.fapi.photos_licenses_getInfo() + if not rsp: + log.error("couldn't retrieve licenses; got error %s", rsp.errormsg) + return None + licenseDict = {} + for l in rsp.licenses[0].license: + licenseDict[l['id']] = l['name'] + keys = licenseDict.keys() + keys.sort() + sortedLicenseList = [] + for k in keys: + # Add tuple of license key, and license value. + sortedLicenseList.append((k, licenseDict[k])) + return sortedLicenseList + + def setLicense(self, photoId, license): + log.debug("id: %s, license: %s", photoId, license) + rsp = self.fapi.photos_licenses_setLicense(auth_token=self.authtoken, + photo_id=photoId, + license_id=license) + if not rsp: + log.error("couldn't set license info for photo %s; got error %s", + photoId, rsp.errormsg) + return False + return True + + def getPhoto(self, photoId): + log.debug("id: %s", photoId) + rsp = self.fapi.photos_getSizes(auth_token=self.authtoken, + photo_id=photoId) + if not rsp: + log.error("error while trying to retrieve size information" + " for photo %s", photoId) + return None + buf = "" + for a in rsp.sizes[0].size: + if a['label']=='Original': + try: + f = urllib2.urlopen(a['source']) + buf = f.read() + except: + log.error("exception in getPhoto") + log.error(format_exc()) + return "" + if not buf: + f = urllib2.urlopen(rsp.sizes[0].size[-1]['source']) + buf = f.read() + return buf + + def removePhotofromSet(self, photoId, photosetId): + log.debug("id: %s, setid: %s", photoId, photosetId) + rsp = self.fapi.photosets_removePhoto(auth_token=self.authtoken, + photo_id=photoId, + photoset_id=photosetId) + if rsp: + log.info("photo %s removed from set %s", photoId, photosetId) + else: + log.error(rsp.errormsg) + + + def getBandwidthInfo(self): + log.debug("retrieving bandwidth information") + rsp = self.fapi.people_getUploadStatus(auth_token=self.authtoken) + if not rsp: + log.error("can't retrieve bandwidth information; got error %s", + rsp.errormsg) + return (None,None) + bw = rsp.user[0].bandwidth[0] + log.debug("max bandwidth: %s, bandwidth used: %s", bw['max'], bw['used']) + return (bw['max'], bw['used']) + + def getUserId(self): + log.debug("entered") + rsp = self.fapi.auth_checkToken(api_key=flickrAPIKey, + auth_token=self.authtoken) + if not rsp: + log.error("unable to get userid; got error %s", rsp.errormsg) + return None + usr = rsp.auth[0].user[0] + log.info("got NSID %s", usr['nsid']) + #Set self.user_id to this value + self.user_id = usr['nsid'] + return usr['nsid'] + + def getPhotosetList(self): + log.debug("entered") + if self.user_id is "": + self.getUserId() #This will set the value of self.user_id + rsp = self.fapi.photosets_getList(auth_token=self.authtoken, + user_id=self.user_id) + if not rsp: + log.error("error getting photoset list; got error %s", rsp.errormsg) + return [] + if not hasattr(rsp.photosets[0], "photoset"): + log.info("no sets found for userid %s", self.user_id) + return [] + else: + log.info("%s sets found for userid %s", + len(rsp.photosets[0].photoset), self.user_id) + return rsp.photosets[0].photoset + + def parseInfoFromPhoto(self, photo, perms=None): + info = {} + info['id'] = photo['id'] + info['title'] = photo['title'].replace('/', '_') + # Some pics don't contain originalformat attribute, so set it to jpg by default. + try: + info['format'] = photo['originalformat'] + except KeyError: + info['format'] = 'jpg' + + try: + info['dupload'] = photo['dateupload'] + except KeyError: + info['dupload'] = '0' + + try: + info['dupdate'] = photo['lastupdate'] + except KeyError: + info['dupdate'] = '0' + + info['perms'] = perms + return info + + def parseInfoFromFullInfo(self, id, fullInfo): + info = {} + info['id'] = id + info['title'] = fullInfo[4] + info['format'] = fullInfo[0] + info['dupload'] = fullInfo[10] + info['dupdate'] = fullInfo[11] + info['mode'] = fullInfo[1] + return info + + def getPhotosFromPhotoset(self, photoset_id): + log.debug("set id: %s", photoset_id) + photosPermsMap = {} + # I'm not utilizing the value part of this dictionary. Its arbitrarily + # set to i. + for i in range(0,3): + page = 1 + while True: + rsp = self.fapi.photosets_getPhotos(auth_token=self.authtoken, + photoset_id=photoset_id, + extras=self.extras, + page=str(page), + privacy_filter=str(i)) + if not rsp: + break + if not hasattr(rsp.photoset[0], 'photo'): + log.error("photoset %s doesn't have attribute photo", rsp.photoset[0]['id']) + break + for p in rsp.photoset[0].photo: + photosPermsMap[p] = str(i) + page += 1 + if page > int(rsp.photoset[0]['pages']): break + if photosPermsMap: break + return photosPermsMap + + def getPhotoStream(self, user_id): + log.debug("userid: %s", user_id) + retList = [] + pageNo = 1 + maxPage = 1 + while pageNo<=maxPage: + log.info("retreiving page number %s of %s", pageNo, maxPage) + rsp = self.fapi.photos_search(auth_token=self.authtoken, + user_id=user_id, per_page="500", + page=str(pageNo), extras=self.extras) + if not rsp: + log.error("can't retrive photos from your stream; got error %s", + rsp.errormsg) + return retList + if not hasattr(rsp.photos[0], 'photo'): + log.error("photos.search response doesn't have attribute photos; " + "returning list acquired so far") + return retList + for a in rsp.photos[0].photo: + retList.append(a) + maxPage = int(rsp.photos[0]['pages']) + pageNo = pageNo + 1 + return retList + + def getTaggedPhotos(self, tags, user_id=None): + log.debug("tags: %s user_id: %s", tags, user_id) + kw = kwdict(auth_token=self.authtoken, tags=tags, tag_mode="all", + extras=self.extras, per_page="500") + if user_id is not None: + kw = kwdict(user_id=user_id, **kw) + rsp = self.fapi.photos_search(**kw) + log.debug("search for photos with tags %s has been" + " successfully finished" % tags) + if not rsp: + log.error("couldn't search for the photos; got error %s", rsp.errormsg) + return + if not hasattr(rsp.photos[0], 'photo'): + return [] + return rsp.photos[0].photo diff -Naur flickrfs-1.3.9.orig/flickrfs.py flickrfs-cvs.orig/flickrfs.py --- flickrfs-1.3.9.orig/flickrfs.py 2007-02-04 06:08:09.000000000 +0100 +++ flickrfs-cvs.orig/flickrfs.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,1084 +0,0 @@ -#=============================================================================== -# flickrfs - Virtual Filesystem for Flickr -# Copyright (c) 2005,2006 Manish Rai Jain -# -# This program can be distributed under the terms of the GNU GPL version 2, or -# its later versions. -# -# DISCLAIMER: The API Key and Shared Secret are provided by the author in -# the hope that it will prevent unnecessary trouble to the end-user. The -# author will not be liable for any misuse of this API Key/Shared Secret -# through this application/derived apps/any 3rd party apps using this key. -#=============================================================================== - -__author__ = "Manish Rai Jain (manishrjain@gmail.com)" -__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" - -import thread, string, ConfigParser, mimetypes, codecs -import time, logging, logging.handlers, os, sys -from glob import glob -from errno import * -from traceback import format_exc -from fuse import Fuse -import threading -import random, commands -from urllib2 import URLError -from transactions import TransFlickr -import inodes - -#Some global definitions and functions -NUMRETRIES = 3 - -#Set up the .flickfs directory. -homedir = os.getenv('HOME') -flickrfsHome = os.path.join(homedir, '.flickrfs') -dbPath = os.path.join(flickrfsHome, '.inode.bdb') - -if not os.path.exists(flickrfsHome): - os.mkdir(os.path.join(flickrfsHome)) -else: - # Remove previous metadata files from ~/.flickrfs - for a in glob(os.path.join(flickrfsHome, '.*')): - os.remove(os.path.join(flickrfsHome, a)) - try: - os.remove(dbPath) - except: - pass - -# Added by Varun Hiremath -if not os.path.exists(flickrfsHome + "/config.txt"): - fconfig = open(flickrfsHome+"/config.txt",'w') - fconfig.write("[configuration]\n") - fconfig.write("browser:/usr/bin/firefox\n") - fconfig.write("image.size:\n") - fconfig.write("sets.sync.int:300\n") - fconfig.write("stream.sync.int:300\n") - fconfig.close() - -# Set up logging -log = logging.getLogger('flickrfs') -loghdlr = logging.handlers.RotatingFileHandler( - os.path.join(flickrfsHome,'log'), "a", 5242880, 3) -logfmt = logging.Formatter("%(asctime)s %(levelname)-10s %(message)s", "%x %X") -loghdlr.setFormatter(logfmt) -log.addHandler(loghdlr) -log.setLevel(logging.DEBUG) - -cp = ConfigParser.ConfigParser() -cp.read(flickrfsHome + '/config.txt') -_resizeStr = "" -sets_sync_int = 600.0 -stream_sync_int = 600.0 -try: - _resizeStr = cp.get('configuration', 'image.size') -except: - print 'No default size of image found. Will upload original size of images.' -try: - sets_sync_int = float(cp.get('configuration', 'sets.sync.int')) -except: - pass -try: - stream_sync_int = float(cp.get('configuration', 'stream.sync.int')) -except: - pass -try: - browserName = cp.get('configuration', 'browser') -except: - pass - -# Retrive the resize string. -def GetResizeStr(): - return _resizeStr - -#Utility functions. -def _log_exception_wrapper(func, *args, **kw): - """Call 'func' with args and kws and log any exception it throws. - """ - for i in range(0, NUMRETRIES): - log.debug("Retry attempt %s for func %s" % (i, func)) - try: - func(*args, **kw) - return - except: - log.error("Exception in function %s" % func) - log.error(format_exc()) - -def background(func, *args, **kw): - """Run 'func' as a thread, logging any exceptions it throws. - - To run - - somefunc(arg1, arg2='value') - - as a thread, do: - - background(somefunc, arg1, arg2='value') - - Any exceptions thrown are logged as errors, and the traceback is logged. - """ - thread.start_new_thread(_log_exception_wrapper, (func,)+args, kw) - -def timerThread(func, func1, interval): - '''Execute func now, followed by func1 every interval seconds - ''' - t = threading.Timer(0.0, func) - try: - t.run() - except: pass - while(interval): - t = threading.Timer(interval, func1) - try: - t.run() - except: pass - -def retryFlickrOp(isNone, func, *args): - # This function helps in retrying the flickr transactions, in case they fail. - result = None - for i in range(0, NUMRETRIES): - log.debug("Retry attempt %s for func %s" % (i, func)) - try: - result = func(*args) - if result is None: - if isNone: - return result - else: - continue - else: - return result - except URLError, detail: - log.error("Failure in function %s with error: %s" % (func, detail)) - # We've utilized all our attempts, send out the result whatever it is. - return result - -class Flickrfs(Fuse): - - def __init__(self, *args, **kw): - - Fuse.__init__(self, *args, **kw) - log.info("flickrfs.py:Flickrfs:mountpoint: %s" % repr(self.mountpoint)) - log.info("flickrfs.py:Flickrfs:unnamed mount options: %s" % self.optlist) - log.info("flickrfs.py:Flickrfs:named mount options: %s" % self.optdict) - - self.inodeCache = inodes.InodeCache(dbPath) # Inodes need to be stored. - self.imgCache = inodes.ImageCache() - self.NSID = "" - self.transfl = TransFlickr(log, browserName) - - # Set some variables to be utilized by statfs function. - self.statfsCounter = -1 - self.max = 0L - self.used = 0L - - self.NSID = self.transfl.getUserId() - if self.NSID is None: - log.error("Initialization:Can't retrieve user information") - sys.exit(-1) - - log.info('Getting list of licenses available') - self.licenses = self.transfl.getLicenses() - if self.licenses is None: - log.error("Initialization:Can't retrieve license information") - sys.exit(-1) - - # do stuff to set up your filesystem here, if you want - self._mkdir("/") - self._mkdir("/tags") - self._mkdir("/tags/personal") - self._mkdir("/tags/public") - background(timerThread, self.sets_thread, - self.sync_sets_thread, sets_sync_int) #sync every 2 minutes - - - def imageResize(self, bufData): - # If no resizing information is present, then return the buffer directly. - if GetResizeStr() == "": - return bufData - - # Else go ahead and do the conversion. - im = '/tmp/flickrfs-' + str(int(random.random()*1000000000)) - f = open(im, 'w') - f.write(bufData) - f.close() - cmd = 'identify -format "%%w" %s'%(im,) - status,ret = commands.getstatusoutput(cmd) - if status!=0: - print "identify command not found. Install Imagemagick" - log.error("identify command not found. Install Imagemagick") - return bufData - try: - if int(ret)0: - log.info('%s photos have been deleted online' % len(psetLocal)) - for c in psetLocal: - log.info('deleting:%s' % c) - self.unlink("%s/%s" % (curdir, c), False) - - def __sync_set_in_background(self, set_id, curdir): - # Exception handling will be done by background function. - log.info("Syncing set %s" % curdir) - psetOnline = self.transfl.getPhotosFromPhotoset(set_id) - self._sync_code(psetOnline, curdir) - log.info("Set %s sync successfully finished." % curdir) - - def sync_sets_thread(self): - log.info("sync_sets_thread: started") - setListOnline = self.transfl.getPhotosetList() - setListLocal = self.getdir('/sets', False) - - for a in setListOnline: - title = a.title[0].elementText.replace('/', '_') - if title.strip()=="": - title = a['id'] - if (title,0) not in setListLocal: #New set added online - log.info("%s set has been added online."%(title,)) - self._mkdir('/sets/'+title, a['id']) - else: #Present Online - setListLocal.remove((title,0)) - for a in setListLocal: #List of sets present locally, but not online - log.info('Recursively deleting set %s'%(a,)) - self.rmdir('/sets/'+a[0], online=False, recr=True) - - for a in setListOnline: - title = a.title[0].elementText.replace('/', '_') - curdir = "/sets/" + title - if title.strip()=='': - curdir = "/sets/" + a['id'] - set_id = a['id'] - background(self.__sync_set_in_background, set_id, curdir) - log.info('sync_sets_thread finished') - - def sync_stream_thread(self): - log.info('sync_stream_thread started') - psetOnline = self.transfl.getPhotoStream(self.NSID) - self._sync_code(psetOnline, '/stream') - log.info('sync_stream_thread finished') - - def stream_thread(self): - log.info("stream_thread started") - print "Populating photostream" - for b in self.transfl.getPhotoStream(self.NSID): - info = self.transfl.parseInfoFromPhoto(b) - self._mkfileWithMeta('/stream', info) - log.info("stream_thread finished") - print "Photostream population finished." - - def tags_thread(self, path): - ind = string.rindex(path, '/') - tagName = path[ind+1:] - if tagName.strip()=='': - log.error("The tagName:%s: doesn't contain any tags"%(tagName)) - return - log.info("tags_thread:" + tagName + ":started") - sendtagList = ','.join(tagName.split(':')) - if(path.startswith('/tags/personal')): - user_id = self.NSID - else: - user_id = None - for b in self.transfl.getTaggedPhotos(sendtagList, user_id): - info = self.transfl.parseInfoFromPhoto(b) - self._mkfileWithMeta(path, info) - - def getUnixPerms(self, info): - mode = info.get('mode') - if mode is not None: - return mode - perms = info.get('perms') - if perms is None: - return 0644 - if perms is "1": # public - return 0755 - elif perms is "2": # friends only. Add 1 to 4 in middle letter. - return 0754 - elif perms is "3": # family only. Add 2 to 4 in middle letter. - return 0764 - elif perms is "4": # friends and family. Add 1+2 to 4 in middle letter. - return 0774 - else: - return 0744 # private - - def __getImageTitle(self, title, id, format = "jpg"): - temp = title.replace('/', '') -# return "%s_%s.%s" % (temp[:32], id, format) - # Store the photos original name. Thus, when pictures are uploaded - # their names would remain as it is, allowing easy resumption of - # uploading of images, in case some of the photos fail uploading. - return "%s.%s" % (temp, format) - - def _mkfileWithMeta(self, path, info): - # Don't write the meta information here, because it requires access to - # the full INFO. Only do with the smaller version of information that - # is provided. - if info is None: - return - title = info.get("title", "") - id = info.get("id", "") - ext = info.get("format", "jpg") - title = self.__getImageTitle(title, id, ext) - - # Refactor this section of code, so that it can be called - # from read. - # Figure out a way to retrieve information, which can be - # used in _mkfile. - mtime = info.get("dupdate") - ctime = info.get("dupload") - perms = self.getUnixPerms(info) - self._mkfile(path+"/"+title, id=id, mode=perms, mtime=mtime, ctime=ctime) - self._mkfile(path+'/.'+title+'.meta', id) - - def _parsepathid(self, path, id=""): - #Path and Id may be unicode strings, so encode them to utf8 now before - #we use them, otherwise python will throw errors when we combine them - #with regular strings. - path = path.encode('utf8') - if id!=0: id = id.encode('utf8') - parentDir, name = os.path.split(path) - if parentDir=='': - parentDir = '/' - log.debug("parentDir:" + parentDir + ":") - return path, id, parentDir, name - - def _mkdir(self, path, id="", mtime=None, ctime=None): - path, id, parentDir, name = self._parsepathid(path, id) - log.debug("Creating directory:" + path) - self.inodeCache[path] = inodes.DirInode(path, id, mtime=mtime, ctime=ctime) - if path!='/': - pinode = self.getInode(parentDir) - pinode.nlink += 1 - self.updateInode(parentDir, pinode) - log.debug("nlink of %s is now %s" % (parentDir, pinode.nlink)) - - def _mkfile(self, path, id="", mode=None, - comm_meta="", mtime=None, ctime=None): - path, id, parentDir, name = self._parsepathid(path, id) - log.debug("Creating file:" + path + ":with id:" + id) - image_name, extension = os.path.splitext(name) - if not extension: - log.error("Can't create file without extension") - return - fInode = inodes.FileInode(path, id, mode=mode, comm_meta=comm_meta, - mtime=mtime, ctime=ctime) - self.inodeCache[path] = fInode - # Now create the meta info inode if the meta info file exists - # refactoring: create the meta info inode, regardless of the - # existence of datapath. -# path = os.path.join(parentDir, '.' + image_name + '.meta') -# datapath = os.path.join(flickrfsHome, '.'+id) -# if os.path.exists(datapath): -# size = os.path.getsize(datapath) -# self.inodeCache[path] = FileInode(path, id) - - def getattr(self, path): - # getattr is being called 4-6 times every second for '/' - # Don't log those calls, as they clutter up the log file. - if path != "/": - log.debug("getattr:" + path + ":") - templist = path.split('/') - if path.startswith('/sets/'): - templist[2] = templist[2].split(':')[0] - elif path.startswith('/stream'): - templist[1] = templist[1].split(':')[0] - path = '/'.join(templist) - - inode=self.getInode(path) - if inode: - #log.debug("inode "+str(inode)) - statTuple = (inode.mode,inode.ino,inode.dev,inode.nlink, - inode.uid,inode.gid,inode.size,inode.atime,inode.mtime,inode.ctime) - #log.debug("statsTuple "+str(statTuple)) - return statTuple - else: - e = OSError("No such file"+path) - e.errno = ENOENT - raise e - - def readlink(self, path): - log.debug("readlink") - return os.readlink(path) - - def getdir(self, path, hidden=True): - log.debug("getdir:" + path) - templist = [] - if hidden: - templist = ['.', '..'] - for a in self.inodeCache.keys(): - ind = a.rindex('/') - if path=='/': - path="" - if path==a[:ind]: - name = a.split('/')[-1] - if name=="": - continue - if hidden and name.startswith('.'): - templist.append(name) - elif not name.startswith('.'): - templist.append(name) - return map(lambda x: (x,0), templist) - - def unlink(self, path, online=True): - log.debug("unlink:%s:" % (path)) - if self.inodeCache.has_key(path): - inode = self.inodeCache.pop(path) - # Remove the meta data file as well if it exists - if self.inodeCache.has_key(path + ".meta"): - self.inodeCache.pop(path + ".meta") - - typesinfo = mimetypes.guess_type(path) - if typesinfo[0] is None or typesinfo[0].count('image')<=0: - log.debug("unlinked a non-image file:%s:"%(path,)) - return - - if path.startswith('/sets/'): - ind = path.rindex('/') - pPath = path[:ind] - pinode = self.getInode(pPath) - if online: - self.transfl.removePhotofromSet(photoId=inode.photoId, - photosetId=pinode.setId) - log.info("Photo %s removed from set"%(path,)) - del inode - else: - log.error("Can't find what you want to remove") - #Dont' raise an exception. Not useful when - #using editors like Vim. They make loads of - #crap buffer files - - def rmdir(self, path, online=True, recr=False): - log.debug("rmdir:%s:"%(path)) - if self.inodeCache.has_key(path): - for a in self.inodeCache.keys(): - if a.startswith(path+'/'): - if recr: - self.unlink(a, online) - else: - e = OSError("Directory not empty") - e.errno = ENOTEMPTY - raise e - else: - log.error("Can't find the directory you want to remove") - e = OSError("No such folder"+path) - e.errno = ENOENT - raise e - - if path=='/sets' or path=='/tags' or path=='/tags/personal' \ - or path=='/tags/public' or path=='/stream': - log.debug("rmdir on the framework! I refuse to do anything! ") - e = OSError("Removal of folder %s not allowed" % (path)) - e.errno = EPERM - raise e - - ind = path.rindex('/') - pPath = path[:ind] - inode = self.inodeCache.pop(path) - if online and path.startswith('/sets/'): - self.transfl.deleteSet(inode.setId) - del inode - pInode = self.getInode(pPath) - pInode.nlink -= 1 - self.updateInode(pPath, pInode) - - def symlink(self, path, path1): - log.debug("symlink") - return os.symlink(path, path1) - - def rename(self, path, path1): - log.debug("rename:path:%s:to path1:%s:"%(path,path1)) - #Donot allow Vim to create a file~ - #Check for .meta in both paths - if path.count('~')>0 or path1.count('~')>0: - log.debug("This seems Vim working") - try: - #Get inode, but _dont_ remove from cache - inode = self.getInode(path) - if inode is not None: - self.inodeCache[path1] = inode - except: - log.debug("Couldn't find inode for:%s:"%(path,)) - return - - #Read from path - inode = self.getInode(path) - if inode is None or not hasattr(inode, 'photoId'): - return - fname = os.path.join(flickrfsHome, '.'+inode.photoId) - f = open(fname, 'r') - buf = f.read() - f.close() - - #Now write to path1 - inode = self.getInode(path1) - if inode is None or not hasattr(inode, 'photoId'): - return - fname = os.path.join(flickrfsHome, '.'+inode.photoId) - f = open(fname, 'w') - f.write(buf) - f.close() - inode.size = os.path.getsize(fname) - self.updateInode(path1, inode) - retinfo = self.parse(fname, inode.photoId) - if retinfo.count('Error')>0: - log.error(retinfo) - - def link(self, srcpath, destpath): - log.debug("link: %s:%s"%(srcpath, destpath)) - #Add image from stream to set, w/o retrieving - slist = srcpath.split('/') - sname_file = slist.pop(-1) - dlist = destpath.split('/') - dname_file = dlist.pop(-1) - error = 0 - if sname_file=="" or sname_file.startswith('.'): - error = 1 - if dname_file=="" or dname_file.startswith('.'): - error = 1 - if not destpath.startswith('/sets/'): - error = 1 - if error is 1: - log.error("Linking is allowed only between 2 image files") - return - sinode = self.getInode(srcpath) - self._mkfile(destpath, id=sinode.id, mode=sinode.mode, - comm_meta=sinode.comm_meta, mtime=sinode.mtime, - ctime=sinode.ctime) - parentPath = '/'.join(dlist) - pinode = self.getInode(parentPath) - if pinode.setId==0: - try: - pinode.setId = self.transfl.createSet(parentPath, sinode.photoId) - self.updateInode(parentPath, pinode) - except: - e = OSError("Can't create a new set") - e.errno = EIO - raise e - else: - self.transfl.put2Set(pinode.setId, sinode.photoId) - - - def chmod(self, path, mode): - log.debug("chmod:%s" % path) - inode = self.getInode(path) - typesinfo = mimetypes.guess_type(path) - - if inode.comm_meta is None: - log.debug("chmod on directory? No use la!") - return - - elif typesinfo[0] is None or typesinfo[0].count('image')<=0: - - os.chmod(path, mode) - return - - elif self.transfl.setPerm(inode.photoId, mode, inode.comm_meta)==True: - inode.mode = mode - self.updateInode(path, inode) - return - - def chown(self, path, user, group): - log.debug("chown. Are you of any use in flickrfs?") - - def truncate(self, path, size): - log.debug("truncate?? Okay okay! I accept your usage:%s:%s"%(path,size)) - ind = path.rindex('/') - name_file = path[ind+1:] - - typeinfo = mimetypes.guess_type(path) - if typeinfo[0] is None or typeinfo[0].count('image')<=0: - inode = self.getInode(path) - filePath = os.path.join(flickrfsHome, '.'+inode.photoId) - f = open(filePath, 'w+') - return f.truncate(size) - - def mknod(self, path, mode, dev): - """ Python has no os.mknod, so we can only do some things """ - log.debug("mknod? OK! Had a close encounter!!:%s:"%(path,)) - templist = path.split('/') - name_file = templist[-1] - - if name_file.startswith('.') and name_file.count('.meta') > 0: - # We need to handle the special case, where some meta files are being - # created through mknod. Creation of meta files is done when adding - # images automatically; and they should not go through mknod system call. - # Editors like Vim, try to generate random swap files when reading - # meta; and this should be *disallowed*. - log.debug("mknod for meta file %s? No use!" % path) - return - - if path.startswith('/sets/'): - templist[2] = templist[2].split(':')[0] - elif path.startswith('/stream'): - templist[1] = templist[1].split(':')[0] - path = '/'.join(templist) - - log.debug("mknod: Modified file path %s" % path) - #Lets guess what kind of a file is this. - #Is it an image file? or, some other temporary file - #created by the tools you're using. - typeinfo = mimetypes.guess_type(path) - if typeinfo[0] is None or typeinfo[0].count('image') <= 0: - f = open(os.path.join(flickrfsHome,'.'+name_file), 'w') - f.close() - # TODO(manishrjain): This should not be FileInode, it should rather be - # Inode. - self.inodeCache[path] = inodes.FileInode(path, name_file, mode=mode) - else: - self._mkfile(path, id="NEW", mode=mode) - - def mkdir(self, path, mode): - log.debug("mkdir:" + path + ":") - if path.startswith("/tags"): - if path.count('/')==3: #/tags/personal (or private)/dirname ONLY - self._mkdir(path) - background(self.tags_thread, path) - else: - e = OSError("Not allowed to create directory %s" % path) - e.errno = EACCES - raise e - elif path.startswith("/sets"): - if path.count('/')==2: #Only allow creation of new set /sets/newset - self._mkdir(path, id=0) - #id=0 means that not yet created online - else: - e = OSError("Not allowed to create directory %s" % path) - e.errno = EACCES - raise e - elif path=='/stream': - self._mkdir(path) - background(timerThread, self.stream_thread, - self.sync_stream_thread, stream_sync_int) - - else: - e = OSError("Not allowed to create directory %s" % path) - e.errno = EACCES - raise e - - def utime(self, path, times): - inode = self.getInode(path) - inode.atime = times[0] - inode.mtime = times[1] - self.updateInode(path, inode) - return 0 - - def open(self, path, flags): - log.info("open: " + path) - ind = path.rindex('/') - name_file = path[ind+1:] - if name_file.startswith('.') and name_file.endswith('.meta'): - self.handleAccessToNonImage(path) - return 0 - typesinfo = mimetypes.guess_type(path) - if typesinfo[0] is None or typesinfo[0].count('image')<=0: - log.debug('open: non-image file found %s' % path) - self.handleAccessToNonImage(path) - return 0 - - templist = path.split('/') - if path.startswith('/sets/'): - templist[2] = templist[2].split(':')[0] - elif path.startswith('/stream'): - templist[1] = templist[1].split(':')[0] - path = '/'.join(templist) - log.debug("open:After modifying:%s:" % (path)) - - inode = self.getInode(path) - if inode.photoId=="NEW": #Just skip if new (i.e. uploading) - return 0 - if self.imgCache.getBuffer(inode.photoId)=="": - log.debug("Retrieving image from flickr: " + inode.photoId) - self.imgCache.setBuffer(inode.photoId, - str(self.transfl.getPhoto(inode.photoId))) - inode.size = self.imgCache.getBufLen(inode.photoId) - log.debug("Size of image: " + str(inode.size)) - self.updateInode(path, inode) - return 0 - - def read(self, path, length, offset): - log.debug("read:%s:offset:%s:length:%s:" % (path,offset,length)) - ind = path.rindex('/') - name_file = path[ind+1:] - if name_file.startswith('.') and name_file.endswith('.meta'): - # Check if file is not present. If not, retrieve and - # create the file locally. - buf = self.handleReadNonImage(path, length, offset) - return buf - typesinfo = mimetypes.guess_type(path) - if typesinfo[0] is None or typesinfo[0].count('image')<=0: - return self.handleReadNonImage(path, length, offset) - return self.handleReadImage(path, length, offset) - - def parse(self, fname, photoId): - cp = ConfigParser.ConfigParser() - log.debug("Parsing file %s" % fname) - cp.read(fname) - log.debug("Read file %s for parsing." % fname) - options = cp.options('metainfo') - title='' - desc='' - tags='' - license='' - if 'description' in options: - desc = cp.get('metainfo', 'description') - if 'tags' in options: - tags = cp.get('metainfo', 'tags') - if 'title' in options: - title = cp.get('metainfo', 'title') - if 'license' in options: - license = cp.get('metainfo', 'license') - - log.debug("Setting metadata for file %s" % fname) - if self.transfl.setMeta(photoId, title, desc)==False: - return "Error:Can't set Meta information" - - log.debug("Setting tags:%s:"%(fname,)) - if self.transfl.setTags(photoId, tags)==False: - log.debug("Setting tags FAILED : %s" % fname) - return "Error:Can't set tags" - - log.debug("Setting license:%s:"%(fname,)) - if self.transfl.setLicense(photoId, license)==False: - return "Error:Can't set license" - - # except: - # log.error("Can't parse file:%s:"%(fname,)) - # return "Error:Can't parse" - return 'Success:Updated photo:%s:%s:'%(fname,photoId) - - ################################################## - # 'handle' Functions for handling read and writes. - ################################################## - def handleAccessToNonImage(self, path): - inode = self.getInode(path) - if inode is None: - log.error("inode doesn't exist:%s:"%(path,)) - e = OSError("No inode found") - e.errno = EIO - raise e - fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext - # Handle the case when file already exists. - if not os.path.exists(fname) or os.path.getsize(fname) == 0L: - log.info("Retrieving meta information for file %s and photo id %s" % - (fname, inode.photoId)) - INFO = self.transfl.getPhotoInfo(inode.photoId) - size = self.writeMetaInfo(inode.photoId, INFO) - log.info("Information has been written for photo id %s" % inode.photoId) - inode.size = size - self.updateInode(path, inode) - time.sleep(1) # Enough time for OS to call for getattr again. - return inode - - def handleReadNonImage(self, path, length, offset): - inode = self.handleAccessToNonImage(path) - f = open(os.path.join(flickrfsHome, '.'+inode.photoId), 'r') - f.seek(offset) - return f.read(length) - - def handleReadImage(self, path, length, offset): - inode = self.getInode(path) - if inode is None: - log.error("inode doesn't exist:%s:"%(path,)) - e = OSError("No inode found") - e.errno = EIO - raise e - if self.imgCache.getBufLen(inode.photoId) is 0: - log.debug("Retrieving image from flickr: " + inode.photoId) - buf = retryFlickrOp(False, self.transfl.getPhoto, - inode.photoId) - if len(buf) == 0: - log.error("Can't retrieve image %s"%(inode.photoId,)) - e = OSError("Unable to retrieve image.") - e.errno = EIO - raise e - self.imgCache.setBuffer(inode.photoId, buf) - inode.size = self.imgCache.getBufLen(inode.photoId) - temp = self.imgCache.getBuffer(inode.photoId, offset, offset+length) - if len(temp) < length: - self.imgCache.popBuffer(inode.photoId) - self.updateInode(path, inode) - return temp - - def handleWriteToNonImage(self, path, buf, off): - inode = self.handleAccessToNonImage(path) - fname = os.path.join(flickrfsHome, '.'+inode.photoId) #ext - log.debug("Writing to :%s:"%(fname,)) - f = open(fname, 'r+') - f.seek(off) - f.write(buf) - f.close() - if len(buf)<4096: - inode.size = os.path.getsize(fname) - retinfo = self.parse(fname, inode.photoId) - if retinfo.count('Error')>0: - e = OSError(retinfo.split(':')[1]) - e.errno = EIO - raise e - self.updateInode(path, inode) - return len(buf) - - def handleUploadingImage(self, path, inode, taglist): - tags = [ '"%s"'%(a,) for a in taglist] - tags.append('flickrfs') - taglist = ' '.join(tags) - log.info('uploading %s with len %s' % - (path, self.imgCache.getBufLen(inode.photoId))) - id = None - bufData = self.imgCache.getBuffer(inode.photoId) - bufData = self.imageResize(bufData) - id = retryFlickrOp(False, self.transfl.uploadfile, - path, taglist, bufData, inode.mode) - if id is None: - log.error("unable to upload file:%s:"%(inode.photoId,)) - e = OSError("Unable to upload file.") - e.errno = EIO - raise e - self.imgCache.popBuffer(inode.photoId) - inode.photoId = id - self.updateInode(path, inode) - return inode - - def handleWriteToBuffer(self, path, buf): - inode = self.getInode(path) - if inode is None: - log.error("inode doesn't exist:%s:"%(path,)) - e = OSError("No inode found") - e.errno = EIO - raise e - self.imgCache.addBuffer(inode.photoId, buf) - return inode - - def handleWriteAddToSet(self, parentPath, pinode, inode): - #Create set if it doesn't exist online (i.e. if id=0) - if pinode.setId is 0: - # Retry creation of set if unsuccessful. - pinode.setId = retryFlickrOp(False, self.transfl.createSet, - parentPath, inode.photoId) - # If the set is created, then return. - if pinode.setId is not None: - self.updateInode(parentPath, pinode) - return - else: - log.error("Unable to create set:%s"%(parentPath,)) - e = OSError("Unable to create set.") - e.errno = EIO - raise e - else: - # If the operation put2Set doesn't throw exception, that means - # that the picture has been successfully added to set. - # Return in that case, retry otherwise. - retryFlickrOp(True, self.transfl.put2Set, - pinode.setId, inode.photoId) - return - - ############################# - # End of 'handle' Functions. - ############################# - - def write(self, path, buf, off): - log.debug("write:%s:%s"%(path, off)) - ind = path.rindex('/') - name_file = path[ind+1:] - if name_file.startswith('.') and name_file.count('.meta')>0: - return self.handleWriteToNonImage(path, buf, off) - typesinfo = mimetypes.guess_type(path) - if typesinfo[0] is None or typesinfo[0].count('image')<=0: - return self.handleWriteToNonImage(path, buf, off) - templist = path.split('/') - inode = None - if path.startswith('/tags'): - e = OSError("Copying to tags not allowed") - e.errno = EIO - raise e - if path.startswith('/stream'): - tags = templist[1].split(':') - templist[1] = tags.pop(0) - path = '/'.join(templist) - inode = self.handleWriteToBuffer(path, buf) - if len(buf) < 4096: - self.handleUploadingImage(path, inode, tags) - elif path.startswith('/sets/'): - setnTags = templist[2].split(':') - setName = setnTags.pop(0) - templist[2] = setName - path = '/'.join(templist) - inode = self.handleWriteToBuffer(path, buf) - if len(buf) < 4096: - templist.pop(-1) - parentPath = '/'.join(templist) - pinode = self.getInode(parentPath) - inode = self.handleUploadingImage(path, inode, setnTags) - self.handleWriteAddToSet(parentPath, pinode, inode) - log.debug("After modifying write:%s:%s"%(path, off)) - if len(buf)<4096: - templist = path.split('/') - templist.pop(-1) - parentPath = '/'.join(templist) - try: - self.inodeCache.pop(path) - except: - pass - INFO = self.transfl.getPhotoInfo(inode.photoId) - info = self.transfl.parseInfoFromFullInfo(inode.photoId, INFO) - self._mkfileWithMeta(parentPath, info) - self.writeMetaInfo(inode.photoId, INFO) - return len(buf) - - def getInode(self, path): - if self.inodeCache.has_key(path): - #log.debug("Got cached inode: " + path) - return self.inodeCache[path] - else: - #log.debug("No inode??? I DIE!!!") - return None - - def updateInode(self, path, inode): - self.inodeCache[path] = inode - - def release(self, path, flags): - log.debug("flickrfs.py:Flickrfs:release: %s %s" % (path,flags)) - return 0 - - def statfs(self): - """ - Should return a tuple with the following elements in respective order: - - F_BSIZE - Preferred file system block size. (int) - F_FRSIZE - Fundamental file system block size. (int) - F_BLOCKS - Total number of blocks in the filesystem. (long) - F_BFREE - Total number of free blocks. (long) - F_BAVAIL - Free blocks available to non-super user. (long) - F_FILES - Total number of file nodes. (long) - F_FFREE - Total number of free file nodes. (long) - F_FAVAIL - Free nodes available to non-super user. (long) - F_FLAG - Flags. System dependent: see statvfs() man page. (int) - F_NAMEMAX - Maximum file name length. (int) - Feel free to set any of the above values to 0, which tells - the kernel that the info is not available. - """ - block_size = 1024 - blocks = 0L - blocks_free = 0L - files = 0L - files_free = 0L - namelen = 255 - # statfs is being called repeatedly at least once a second. - # The bandwidth information doesn't change that often, so - # save upon communication with flickr servers to retrieve this - # information. Only retrieve it once in a while. - if self.statfsCounter >= 500 or self.statfsCounter is -1: - (self.max, self.used) = self.transfl.getBandwidthInfo() - self.statfsCounter = 0 - log.info('statfs: Retrieved Bandwidth info: max %s used %s' % - (self.max, self.used)) - self.statfsCounter = self.statfsCounter + 1 - - if self.max is not None: - blocks = long(self.max)/block_size - blocks_used = long(self.used)/block_size - blocks_free = blocks - blocks_used - blocks_available = blocks_free - return (block_size, blocks, blocks_free, blocks_available, - files, files_free, namelen) - - def fsync(self, path, isfsyncfile): - log.debug("flickrfs.py:Flickrfs:fsync: path=%s, isfsyncfile=%s" % - (path,isfsyncfile)) - return 0 - - -if __name__ == '__main__': - try: - server = Flickrfs() - server.multithreaded = 1; - server.main() - except KeyError: - log.error('Got key error. Exiting...') - sys.exit(0) diff -Naur flickrfs-1.3.9.orig/inodes.py flickrfs-cvs.orig/inodes.py --- flickrfs-1.3.9.orig/inodes.py 2007-02-04 06:08:09.000000000 +0100 +++ flickrfs-cvs.orig/inodes.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,134 +0,0 @@ -#=============================================================================== -# flickrfs - Virtual Filesystem for Flickr -# Copyright (c) 2005,2006 Manish Rai Jain -# -# This program can be distributed under the terms of the GNU GPL version 2, or -# its later versions. -# -# DISCLAIMER: The API Key and Shared Secret are provided by the author in -# the hope that it will prevent unnecessary trouble to the end-user. The -# author will not be liable for any misuse of this API Key/Shared Secret -# through this application/derived apps/any 3rd party apps using this key. -#=============================================================================== - -__author__ = "Manish Rai Jain (manishrjain@gmail.com)" -__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" - -import os, sys, time -from stat import * -import cPickle - -DEFAULTBLOCKSIZE = 4*1024 # 4 KB - -class Inode(object): - """Common base class for all file system objects - """ - def __init__(self, path=None, id='', mode=None, - size=0L, mtime=None, ctime=None): - self.nlink = 1 - self.size = size - self.id = id - self.mode = mode - self.ino = long(time.time()) - self.dev = 409089L - self.uid = int(os.getuid()) - self.gid = int(os.getgid()) - now = int(time.time()) - self.atime = now - if mtime is None: - self.mtime = now - else: - self.mtime = int(mtime) - if ctime is None: - self.ctime = now - else: - self.ctime = int(ctime) - self.blocksize = DEFAULTBLOCKSIZE - -class DirInode(Inode): - def __init__(self, path=None, id="", mode=None, mtime=None, ctime=None): - if mode is None: mode = 0755 - super(DirInode, self).__init__(path, id, mode, 0L, mtime, ctime) - self.mode = S_IFDIR | self.mode - self.nlink += 1 - self.dirfile = "" - self.setId = self.id - - -class FileInode(Inode): - def __init__(self, path=None, id="", mode=None, comm_meta="", - size=0L, mtime=None, ctime=None): - if mode is None: mode = 0644 - super(FileInode, self).__init__(path, id, mode, size, mtime, ctime) - self.mode = S_IFREG | self.mode - self.photoId = self.id - self.comm_meta = comm_meta - - -class ImageCache: - def __init__(self): - self.bufDict = {} - - def setBuffer(self, id, buf): - self.bufDict[id] = buf - - def addBuffer(self, id, inc): - buf = self.getBuffer(id) - self.setBuffer(id, buf+inc) - - def getBuffer(self, id, start=0, end=0): - if end == 0: - return self.bufDict.get(id, "")[start:] - else: - return self.bufDict.get(id, "")[start:end] - - def getBufLen(self, id): - return long(len(self.bufDict.get(id, ""))) - - def popBuffer(self, id): - if id in self.bufDict: - return self.bufDict.pop(id) - - -class InodeCache(dict): - def __init__(self, dbPath): - dict.__init__(self) - try: - import bsddb - # If bsddb is available, utilize that package - # and store the inodes in database. - self.db = bsddb.btopen(dbPath, flag='c') - except: - # Otherwise, store the inodes in memory. - self.db = {} - # Keep the keys in memory. - self.keysCache = set() - - def __getitem__(self, key, d=None): - # key k may be unicode, so convert it to - # a normal string first. - if not self.has_key(key): - return d - valObjStr = self.db.get(str(key)) - return cPickle.loads(valObjStr) - - def __setitem__(self, key, value): - self.keysCache.add(key) - self.db[str(key)] = cPickle.dumps(value) - - def get(self, k, d=None): - return self.__getitem__(k, d) - - def keys(self): - return list(self.keysCache) - - def pop(self, k, *args): - # key k may be unicode, so convert it to - # a normal string first. - valObjStr = self.db.pop(str(k), *args) - self.keysCache.discard(k) - if valObjStr != None: - return cPickle.loads(valObjStr) - - def has_key(self, k): - return k in self.keysCache diff -Naur flickrfs-1.3.9.orig/README flickrfs-cvs.orig/README --- flickrfs-1.3.9.orig/README 2007-02-04 06:09:01.000000000 +0100 +++ flickrfs-cvs.orig/README 2008-01-31 20:32:33.000000000 +0100 @@ -61,3 +61,15 @@ copy of the extenion name tacked on. - bugfix: fixed inability to retrieve certain photos. - bugfix: added missing import of 'sys' module (only used during certain error exits) + - 20080127RDM bugfix: added fuse-python 0.2 compatibility by setting the + python_fuse_api to 0.1. + - 20080128RDM bugfix: use only one thread to populate photos in sets, + rather than one thread per set. This seems to fix the problem of + having no photos appear when there are lots of sets. + - 20080128RDM bugfix: handle the case of the photo not having an + 'originalformat' property in getPhotoInfo (default to jpg). + - 20080129RDM bugfix: removed limit of 500 images per directory + in the 'sets' directory. Bug still exists for other directories. + - 20080131RDM feature: made the logging message formats more consistent, and + made fuller use of the python logging facility features. + - 20080131RDM bugfix: log exceptions in timerThread if they happen diff -Naur flickrfs-1.3.9.orig/setup.py flickrfs-cvs.orig/setup.py --- flickrfs-1.3.9.orig/setup.py 1970-01-01 01:00:00.000000000 +0100 +++ flickrfs-cvs.orig/setup.py 2007-03-29 16:44:25.000000000 +0200 @@ -0,0 +1,25 @@ +###################################################################### +## +## Copyright (C) 2006, Varun Hiremath +## +## Filename: setup.py +## Author: Varun Hiremath +## Description: Installation script +## License: GPL +###################################################################### + +import os, sys +from distutils.core import setup + +PROGRAM_NAME = "flickrfs" +PROGRAM_VERSION = "1.3.9" +PROGRAM_URL = "http://manishrjain.googlepages.com/flickrfs" + +setup(name='%s' % (PROGRAM_NAME).lower(), + version='%s' % (PROGRAM_VERSION), + description="virtual filesystem for flickr online photosharing service", + author="Manish Rai Jain", + license='GPL-2', + url="%s" % (PROGRAM_URL), + author_email=" ", + packages = ['flickrfs']) diff -Naur flickrfs-1.3.9.orig/transactions.py flickrfs-cvs.orig/transactions.py --- flickrfs-1.3.9.orig/transactions.py 2007-02-04 06:08:09.000000000 +0100 +++ flickrfs-cvs.orig/transactions.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,395 +0,0 @@ -#=============================================================================== -# flickrfs - Virtual Filesystem for Flickr -# Copyright (c) 2005,2006 Manish Rai Jain -# -# This program can be distributed under the terms of the GNU GPL version 2, or -# its later versions. -# -# DISCLAIMER: The API Key and Shared Secret are provided by the author in -# the hope that it will prevent unnecessary trouble to the end-user. The -# author will not be liable for any misuse of this API Key/Shared Secret -# through this application/derived apps/any 3rd party apps using this key. -#=============================================================================== - -__author__ = "Manish Rai Jain (manishrjain@gmail.com)" -__license__ = "GPLv2 (details at http://www.gnu.org/licenses/licenses.html#GPL)" - -from flickrapi import FlickrAPI -from traceback import format_exc -import urllib2 -import sys -import string -import os -import time - -# flickr auth information -flickrAPIKey = "f8aa9917a9ae5e44a87cae657924f42d" # API key -flickrSecret = "3fbf7144be7eca28" # shared "secret" - -# Utility functions -def kwdict(**kw): return kw - -#Transactions with flickr, wraps FlickrAPI -# calls in Flickfs-specialized functions. -class TransFlickr: - - extras = "original_format,date_upload,last_update" - - def __init__(self, logg, browserName): - global log - log = logg - self.fapi = FlickrAPI(flickrAPIKey, flickrSecret) - self.user_id = "" - # proceed with auth - # TODO use auth.checkToken function if available, - # and wait after opening browser. - print "Authorizing with flickr..." - log.info("Authorizing with flickr...") - try: - self.authtoken = self.fapi.getToken(browser=browserName) - except: - print ("Can't retrieve token from browser:%s:"%(browserName,)) - print ("\tIf you're behind a proxy server," - " first set http_proxy environment variable.") - print "\tPlease close all your browser windows, and try again" - log.error(format_exc()) - log.error("Can't retrieve token from browser:%s:"%(browserName,)) - sys.exit(-1) - if self.authtoken == None: - log.error('Not able to authorize. Exiting...') - sys.exit(-1) - #Add some authorization checks here(?) - print "Authorization complete." - log.info('Authorization complete') - - def uploadfile(self, filepath, taglist, bufData, mode): - #Set public 4(always), 1(public). Public overwrites f&f. - public = mode&1 - #Set friends and family 4(always), 2(family), 1(friends). - friends = mode>>3 & 1 - family = mode>>4 & 1 - #E.g. 745 - 4:No f&f, but 5:public - #E.g. 754 - 5:friends, but not public - #E.g. 774 - 7:f&f, but not public - - log.info("Uploading file %s with data of len %s" % (filepath, len(bufData))) - log.info("and tags %s" % str(taglist)) - log.info("Permissions: Family %s Friends %s Public %s" % - (family,friends,public)) - filename = os.path.splitext(os.path.basename(filepath))[0] - rsp = self.fapi.upload(filename=filepath, jpegData=bufData, - title=filename, - tags=taglist, - is_public=public and "1" or "0", - is_friend=friends and "1" or "0", - is_family=family and "1" or "0") - - if rsp is None: - log.error("Seems like we could't write file %s." - " Running checks." % filepath) - recent_rsp = None - trytimes = 2 - while(trytimes): - log.info("Trying to retrieve the recently updated photo." - " Sleeping for 3 seconds...") - time.sleep(3) - trytimes -= 1 - # Keep on trying to retrieve the recently uploaded photo, till we - # actually get the information, or the function throws an exception. - while(recent_rsp is None or not recent_rsp): - recent_rsp = self.fapi.photos_recentlyUpdated( - auth_token=self.authtoken, min_date='1', per_page='1') - - pic = recent_rsp.photos[0].photo[0] - log.info('Pic looking for is %s' % filename) - log.info('Most recently updated pic is %s' % pic['title']) - if filename == pic['title']: - id = pic['id'] - log.info("File uploaded %s with photoid %s" % (filepath, id)) - return id - return None - else: - id = rsp.photoid[0].elementText - log.info("File uploaded %s with photoid %s" % (filepath, id)) - return id - - def put2Set(self, set_id, photo_id): - log.info("Uploading photo %s to set id %s" % (photo_id, set_id)) - rsp = self.fapi.photosets_addPhoto(auth_token=self.authtoken, - photoset_id=set_id, photo_id=photo_id) - if rsp: - log.info("Uploaded photo to set") - else: - log.error(rsp.errormsg) - - def createSet(self, path, photo_id): - log.info("Creating set %s with primary photo %s" % (path,photo_id)) - path, title = os.path.split(path) - rsp = self.fapi.photosets_create(auth_token=self.authtoken, - title=title, primary_photo_id=photo_id) - if rsp: - log.info("Created set %s" % title) - return rsp.photoset[0]['id'] - else: - log.error(rsp.errormsg) - - def deleteSet(self, set_id): - log.info("Deleting set %s" % set_id) - if str(set_id)=="0": - log.info("The set %s is non-existant online." % set_id) - return - rsp = self.fapi.photosets_delete(auth_token=self.authtoken, - photoset_id=set_id) - if rsp: - log.info("Deleted set %s." % set_id) - else: - log.error(rsp.errormsg) - - def getPhotoInfo(self, photoId): - rsp = self.fapi.photos_getInfo(auth_token=self.authtoken, photo_id=photoId) - if not rsp: - log.error("Can't retrieve information about photo %s, with error %s" % - (photoId, rsp.errormsg)) - return None - format = rsp.photo[0]['originalformat'] - perm_public = rsp.photo[0].visibility[0]['ispublic'] - perm_family = rsp.photo[0].visibility[0]['isfamily'] - perm_friend = rsp.photo[0].visibility[0]['isfriend'] - if perm_public == '1': - mode = 0755 - else: - b_cnt = 4 - if perm_family == '1': - b_cnt += 2 - if perm_friend == '1': - b_cnt += 1 - mode = "07" + str(b_cnt) + "4" - mode = int(mode) - - if hasattr(rsp.photo[0],'permissions'): - permcomment = rsp.photo[0].permissions[0]['permcomment'] - permaddmeta = rsp.photo[0].permissions[0]['permaddmeta'] - else: - permcomment = permaddmeta = [None] - - commMeta = '%s%s' % (permcomment,permaddmeta) # Required for chmod. - desc = rsp.photo[0].description[0].elementText - title = rsp.photo[0].title[0].elementText - if hasattr(rsp.photo[0].tags[0], "tag"): - taglist = [ a.elementText for a in rsp.photo[0].tags[0].tag ] - else: - taglist = [] - license = rsp.photo[0]['license'] - owner = rsp.photo[0].owner[0]['username'] - ownerNSID = rsp.photo[0].owner[0]['nsid'] - url = rsp.photo[0].urls[0].url[0].elementText - posted = rsp.photo[0].dates[0]['posted'] - lastupdate = rsp.photo[0].dates[0]['lastupdate'] - return (format, mode, commMeta, desc, title, taglist, - license, owner, ownerNSID, url, int(posted), int(lastupdate)) - - def setPerm(self, photoId, mode, comm_meta="33"): - public = mode&1 #Set public 4(always), 1(public). Public overwrites f&f - #Set friends and family 4(always), 2(family), 1(friends) - friends = mode>>3 & 1 - family = mode>>4 & 1 - if len(comm_meta)<2: - # This wd patch string index out of range bug, caused - # because some photos may not have comm_meta value set. - comm_meta="33" - rsp = self.fapi.photos_setPerms(auth_token=self.authtoken, - is_public=str(public), - is_friend=str(friends), - is_family=str(family), - perm_comment=comm_meta[0], - perm_addmeta=comm_meta[1], - photo_id=photoId) - if not rsp: - log.error("Couldn't set permission for photo %s with error %s" % - (photoId,rsp.errormsg)) - return False - log.info("Permission has been set for photo %s" % photoId) - return True - - def setTags(self, photoId, tags): - templist = [ '"%s"'%(a,) for a in string.split(tags, ',')] + ['flickrfs'] - tagstring = ' '.join(templist) - rsp = self.fapi.photos_setTags(auth_token=self.authtoken, - photo_id=photoId, tags=tagstring) - if not rsp: - log.error("Couldn't set tags %s with error %s" % - (photoId, rsp.errormsg)) - return False - return True - - def setMeta(self, photoId, title, desc): - rsp = self.fapi.photos_setMeta(auth_token=self.authtoken, - photo_id=photoId, title=title, - description=desc) - if not rsp: - log.error("Couldn't set meta info for photo %s with error" % - (photoId, rsp.errormsg)) - return False - return True - - def getLicenses(self): - rsp = self.fapi.photos_licenses_getInfo() - if not rsp: - log.error("Couldn't retrieve licenses with error %s" % rsp.errormsg) - return None - licenseDict = {} - for l in rsp.licenses[0].license: - licenseDict[l['id']] = l['name'] - keys = licenseDict.keys() - keys.sort() - sortedLicenseList = [] - for k in keys: - # Add tuple of license key, and license value. - sortedLicenseList.append((k, licenseDict[k])) - return sortedLicenseList - - def setLicense(self, photoId, license): - rsp = self.fapi.photos_licenses_setLicense(auth_token=self.authtoken, - photo_id=photoId, - license_id=license) - if not rsp: - log.error("Couldn't set license info for photo %s with error %s" % - (photoId, rsp.errormsg)) - return False - return True - - def getPhoto(self, photoId): - rsp = self.fapi.photos_getSizes(auth_token=self.authtoken, - photo_id=photoId) - if not rsp: - log.error("Error while trying to retrieve size information" - " for photo %s" % photoId) - return None - buf = "" - for a in rsp.sizes[0].size: - if a['label']=='Original': - try: - f = urllib2.urlopen(a['source']) - buf = f.read() - except: - log.error("Exception in getPhoto") - log.error(format_exc()) - return "" - if not buf: - f = urllib2.urlopen(rsp.sizes[0].size[-1]['source']) - buf = f.read() - return buf - - def removePhotofromSet(self, photoId, photosetId): - rsp = self.fapi.photosets_removePhoto(auth_token=self.authtoken, - photo_id=photoId, - photoset_id=photosetId) - if rsp: - log.info("Photo %s removed from set %s" % (photoId, photosetId)) - else: - log.error(rsp.errormsg) - - - def getBandwidthInfo(self): - log.debug("Retrieving bandwidth information") - rsp = self.fapi.people_getUploadStatus(auth_token=self.authtoken) - if not rsp: - log.error("Can't retrieve bandwidth information: %s" % rsp.errormsg) - return (None,None) - bw = rsp.user[0].bandwidth[0] - log.debug("Bandwidth: max:" + bw['max']) - log.debug("Bandwidth: used:" + bw['used']) - return (bw['max'], bw['used']) - - def getUserId(self): - rsp = self.fapi.auth_checkToken(api_key=flickrAPIKey, - auth_token=self.authtoken) - if not rsp: - log.error("Unable to get userid:" + rsp.errormsg) - return None - usr = rsp.auth[0].user[0] - log.info("Got NSID:"+ usr['nsid'] + ":") - #Set self.user_id to this value - self.user_id = usr['nsid'] - return usr['nsid'] - - def getPhotosetList(self): - if self.user_id is "": - self.getUserId() #This will set the value of self.user_id - rsp = self.fapi.photosets_getList(auth_token=self.authtoken, - user_id=self.user_id) - if not rsp: - log.error("Error getting photoset list: %s" % (rsp.errormsg)) - return [] - if not hasattr(rsp.photosets[0], "photoset"): - return [] - return rsp.photosets[0].photoset - - def parseInfoFromPhoto(self, photo, perms=None): - info = {} - info['id'] = photo['id'] - info['title'] = photo['title'].replace('/', '_') - info['format'] = photo['originalformat'] - info['dupload'] = photo['dateupload'] - info['dupdate'] = photo['lastupdate'] - info['perms'] = perms - return info - - def parseInfoFromFullInfo(self, id, fullInfo): - info = {} - info['id'] = id - info['title'] = fullInfo[4] - info['format'] = fullInfo[0] - info['dupload'] = fullInfo[10] - info['dupdate'] = fullInfo[11] - info['mode'] = fullInfo[1] - return info - - def getPhotosFromPhotoset(self, photoset_id): - photosPermsMap = {} - for i in range(1,6): - rsp = self.fapi.photosets_getPhotos(auth_token=self.authtoken, - photoset_id=photoset_id, - extras=self.extras, - privacy_filter=str(i)) - if not rsp: - continue - for p in rsp.photoset[0].photo: - photosPermsMap[p] = str(i) - return photosPermsMap - - def getPhotoStream(self, user_id): - retList = [] - pageNo = 1 - maxPage = 1 - while pageNo<=maxPage: - log.info("maxPage:%s pageNo:%s"%(maxPage, pageNo)) - rsp = self.fapi.photos_search(auth_token=self.authtoken, - user_id=user_id, per_page="500", - page=str(pageNo), extras=self.extras) - if not rsp: - log.error("Can't retrive photos from your stream: %s" % rsp.errormsg) - return retList - if not hasattr(rsp.photos[0], 'photo'): - log.error("Doesn't have attribute photos. Page requested %s" % pageNo) - return retList - for a in rsp.photos[0].photo: - retList.append(a) - maxPage = int(rsp.photos[0]['pages']) - pageNo = pageNo + 1 - return retList - - def getTaggedPhotos(self, tags, user_id=None): - kw = kwdict(auth_token=self.authtoken, tags=tags, tag_mode="all", - extras=self.extras, per_page="500") - if user_id is not None: - kw = kwdict(user_id=user_id, **kw) - rsp = self.fapi.photos_search(**kw) - log.debug("Search for photos with tags %s has been" - " successfully finished." % tags) - if not rsp: - log.error("Couldn't search for the photos: %s" % rsp.errormsg) - return - if not hasattr(rsp.photos[0], 'photo'): - return [] - return rsp.photos[0].photo