Python module providing an easy access to FileDropper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FileDropper.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (c) 2009, Thomas Jost <thomas.jost@gmail.com>
  5. #
  6. # Permission to use, copy, modify, and/or distribute this software for any
  7. # purpose with or without fee is hereby granted, provided that the above
  8. # copyright notice and this permission notice appear in all copies.
  9. #
  10. # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  11. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  12. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  13. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  14. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  15. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  16. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  17. """Interact with FileDropper.com"""
  18. import os.path, re, socket, urllib, urllib2
  19. from types import StringTypes
  20. from BeautifulSoup import BeautifulSoup
  21. import poster.encode, poster.streaminghttp
  22. # Permissions
  23. FD_PERM_PUBLIC = 0
  24. FD_PERM_PASSWORD = 1
  25. FD_PERM_PRIVATE = 2
  26. # Prefix for all the URLs used in the module
  27. FD_URL = "http://www.filedropper.com/"
  28. FD_LOGIN_URL = FD_URL + "login.php"
  29. FD_PREMIUM_URL = FD_URL + "premium.php"
  30. FD_UPLOAD_URL = FD_URL + "index.php?xml=true"
  31. FD_PERM_URL = FD_PREMIUM_URL + "?action=setpermissions&%(varset)s&id=%(id)d"
  32. FD_DELETE_URL = FD_PREMIUM_URL + "?action=delete&id=%d"
  33. # Max allowed size
  34. FD_MAX_SIZE = 5*(1024**3)
  35. # Error codes
  36. FD_ERROR_NOT_LOGGED_IN = 1
  37. FD_ERROR_PERMISSIONS = 2
  38. FD_ERROR_FILE_TOO_BIG = 3
  39. FD_ERROR_UPLOAD = 4
  40. FD_ERROR_INVALID_PASSWORD = 5
  41. FD_ERROR_REQUEST = 6
  42. FD_ERRORS = {
  43. FD_ERROR_NOT_LOGGED_IN : "Not logged in",
  44. FD_ERROR_PERMISSIONS : "Invalid permissions",
  45. FD_ERROR_FILE_TOO_BIG : "File is too big",
  46. FD_ERROR_UPLOAD : "Error while uploading a file",
  47. FD_ERROR_INVALID_PASSWORD: "Invalid password",
  48. FD_ERROR_REQUEST : "Problem with the request",
  49. }
  50. # Regexp used for parsing HTML pages
  51. FD_RE_LIST = re.compile("^unhide\('(\d+)'\)")
  52. class FileDropperException(Exception):
  53. def __init__(self, errno):
  54. self.errno = errno
  55. self.errmsg = FD_ERRORS[errno]
  56. def __str__(self):
  57. return "[FileDropper error %d] %s" % (self.errno, self.errmsg)
  58. # Support for reporting upload progress through a callback.
  59. # This is quite ugly and hackish, but I didn't want to reimplement both
  60. # httplib and urllib2 just to achieve this :)
  61. # Big huge warning: upload_callback is common to all FileDropper instances,
  62. # so beware if you want to use it in a multithreaded environment!
  63. #
  64. # upload_callback must be set to None or to a callable accepting 2 arguments.
  65. # The first one will be the number of sent bytes, the second one the total
  66. # number of bytes to send or -1 if this isn't known in advance (for example
  67. # when reading data from an iterable...)
  68. # Usually, the size is known for headers but not for a file (read as an
  69. # iterable to avoid loading it in RAM).
  70. upload_callback = None
  71. psshc = poster.streaminghttp.StreamingHTTPConnection
  72. class FileDropperHTTPConnection(psshc):
  73. def send(self, value):
  74. """Send ``value`` to the server.
  75. ``value`` can be a string object, a file-like object that supports
  76. a .read() method, or an iterable object that supports a .next()
  77. method.
  78. """
  79. # Based on python 2.6's httplib.HTTPConnection.send()
  80. if self.sock is None:
  81. if self.auto_open:
  82. self.connect()
  83. else:
  84. raise NotConnected()
  85. # send the data to the server. if we get a broken pipe, then close
  86. # the socket. we want to reconnect when somebody tries to send again.
  87. #
  88. # NOTE: we DO propagate the error, though, because we cannot simply
  89. # ignore the error... the caller will know if they can retry.
  90. if self.debuglevel > 0:
  91. print "send:", repr(value)
  92. try:
  93. blocksize=8192
  94. total=-1
  95. try: total=len(value)
  96. except TypeError: pass
  97. sent=0
  98. if hasattr(value,'read') :
  99. if self.debuglevel > 0: print "sendIng a read()able"
  100. print "sendIng a read()able"
  101. data=value.read(blocksize)
  102. while data:
  103. self.sock.sendall(data)
  104. sent+=len(data)
  105. if callable(upload_callback):
  106. upload_callback(sent, total)
  107. data=value.read(blocksize)
  108. elif hasattr(value,'next'):
  109. if self.debuglevel > 0: print "sendIng an iterable"
  110. print "sendIng an iterable"
  111. for data in value:
  112. self.sock.sendall(data)
  113. sent+=len(data)
  114. if callable(upload_callback):
  115. upload_callback(sent, total)
  116. else:
  117. self.sock.sendall(value)
  118. if callable(upload_callback):
  119. upload_callback(total, total)
  120. except socket.error, v:
  121. if v[0] == 32: # Broken pipe
  122. self.close()
  123. raise
  124. poster.streaminghttp.StreamingHTTPConnection = FileDropperHTTPConnection
  125. class FileDropper:
  126. """Builds an empty FileDropper object that may be used for uploading files
  127. to FileDropper.com, get details about files in a premium account, change
  128. their permission or delete them."""
  129. def __init__(self):
  130. self.logged_in = False
  131. # Init the streaming HTTP handler
  132. #register_openers()
  133. # Init the URL opener
  134. #self.url = urllib2.build_opener(urllib2.HTTPCookieProcessor())
  135. self.url = urllib2.build_opener(
  136. poster.streaminghttp.StreamingHTTPHandler,
  137. poster.streaminghttp.StreamingHTTPRedirectHandler,
  138. urllib2.HTTPCookieProcessor
  139. )
  140. def __del__(self):
  141. if self.logged_in:
  142. self.logout()
  143. def login(self, username, password):
  144. """Log into the premium account with the given username and password."""
  145. # Build the data string to send with the POST request
  146. data = urllib.urlencode({"username": username, "password": password})
  147. # Send the request
  148. res = self.url.open(FD_LOGIN_URL, data)
  149. # What is our final URL?
  150. dst_url = res.geturl()
  151. self.logged_in = (res.getcode() == 200) and (dst_url == FD_PREMIUM_URL)
  152. return self.logged_in
  153. def logout(self):
  154. """Log out from a premium account."""
  155. if not self.logged_in:
  156. raise FileDropperException(FD_ERROR_NOT_LOGGED_IN)
  157. self.url.open(FD_URL + "login.php?action=logout")
  158. def list(self):
  159. """Get a list of files in the file manager of a premium account.
  160. The return value is a list of 7-value tuples of the form
  161. (file_name, id, downloads, size, date, permissions, public_url)"""
  162. if not self.logged_in:
  163. raise FileDropperException(FD_ERROR_NOT_LOGGED_IN)
  164. # Download the page
  165. html = self.url.open(FD_PREMIUM_URL).read()
  166. # Parse it
  167. soup = BeautifulSoup(html)
  168. # Get files info
  169. tags = [tag.parent for tag in soup.findAll("a", onclick = FD_RE_LIST)]
  170. files = []
  171. for tag in tags:
  172. # File name in the link
  173. file_name = tag.a.string.strip()
  174. # File ID found using the regexp
  175. m = FD_RE_LIST.search(tag.a['onclick'])
  176. file_id = int(m.group(1))
  177. div = tag.div
  178. # Some dirty searches in an ugly div section
  179. downloads = int(div.contents[2].replace('|', '').strip())
  180. size = div.contents[4].replace('|', '').strip() #TODO: parse it correctly
  181. date = div.contents[6].replace('&nbsp;', '').strip() #TODO: parse it correctly
  182. # Permissions: conversion from 2 strings to a symbol
  183. raw_perm = div.find("span", id="fileperms[%d]" % file_id)
  184. permissions = -1
  185. if raw_perm.span.string == "Private":
  186. permissions = FD_PERM_PRIVATE
  187. elif raw_perm.span.string == "Public":
  188. # If there is a <b> tag, it contains "No password"
  189. if raw_perm.b is not None:
  190. permissions = FD_PERM_PUBLIC
  191. else:
  192. permissions = FD_PERM_PASSWORD
  193. else:
  194. raise FileDropperException(FD_ERROR_PERMISSIONS)
  195. # Public URL (may be published safely anywhere)
  196. public_url = div.find("input", type="text")['value']
  197. value = (file_name, file_id, downloads, size, date, permissions, public_url)
  198. files.append(value)
  199. return files
  200. def upload(self, filename):
  201. """Upload the specified file"""
  202. # Check the file size
  203. if os.path.getsize(filename) > FD_MAX_SIZE:
  204. raise FileDropperException(FD_ERROR_FILE_TOO_BIG)
  205. # Prepare the encoded data
  206. base_name = os.path.basename(filename)
  207. mp1 = poster.encode.MultipartParam("Filename", base_name)
  208. mp2 = poster.encode.MultipartParam("file", filename=base_name, filetype="application/octet-stream", fileobj=open(filename))
  209. data, headers = poster.encode.multipart_encode([mp1, mp2])
  210. # Prepare the request
  211. req = urllib2.Request(FD_UPLOAD_URL, data, headers)
  212. # Send the request
  213. res = self.url.open(req)
  214. # Get the intermediate url
  215. tmp_url = res.read()
  216. #TODO: check if upload failed...
  217. # Get the real file URL... and end with a 404 error :)
  218. try:
  219. res = self.url.open(FD_URL + tmp_url[1:])
  220. except urllib2.HTTPError, exc:
  221. if exc.code == 404:
  222. return exc.geturl()
  223. else:
  224. raise exc
  225. # We should not reach this point as there is supposed to be a 404 error
  226. raise FileDropperException(FD_ERROR_UPLOAD)
  227. def set_perm(self, file_id, perm, password=None):
  228. """Set new permissions for the specified file"""
  229. if not self.logged_in:
  230. raise FileDropperException(FD_ERROR_NOT_LOGGED_IN)
  231. # Prepare the query
  232. query = {'id': file_id}
  233. # Set to public
  234. if perm == FD_PERM_PUBLIC:
  235. query['varset'] = 'public=true'
  236. # There's a weird bug when changing from password-protected
  237. # to public: permissions don't get updated unless we change
  238. # them to private first
  239. self.set_perm(file_id, FD_PERM_PRIVATE)
  240. # Set to private
  241. elif perm == FD_PERM_PRIVATE:
  242. query['varset'] = 'private=true'
  243. # Set to password-protected
  244. elif perm == FD_PERM_PASSWORD:
  245. if (type(password) not in StringTypes) or (password.strip() == ""):
  246. raise FileDropperException(FD_ERROR_INVALID_PASSWORD)
  247. query['varset'] = urllib.urlencode({'password': password})
  248. # Invalid case
  249. else:
  250. raise FileDropperException(FD_ERROR_PERMISSIONS)
  251. # Prepare the request
  252. url = FD_PERM_URL % query
  253. res = self.url.open(url)
  254. txt = res.read()
  255. if res.getcode() != 200:
  256. raise FileDropperException(FD_ERROR_REQUEST)
  257. return txt
  258. def delete(self, file_id):
  259. """Delete the specified file"""
  260. if not self.logged_in:
  261. raise FileDropperException(FD_ERROR_NOT_LOGGED_IN)
  262. # Do the query
  263. res = self.url.open(FD_DELETE_URL % file_id)
  264. txt = res.read()
  265. if res.getcode() != 200:
  266. raise FileDropperException(FD_ERROR_REQUEST)
  267. return txt
  268. if __name__ == "__main__":
  269. from getpass import getpass
  270. import sys
  271. fd = FileDropper()
  272. user = raw_input("Username: ")
  273. if user != "":
  274. password = getpass()
  275. if not fd.login(user, password):
  276. print "Login failed"
  277. print "Current files:"
  278. print fd.list()
  279. print
  280. uploaded_file = fd.upload("test.txt")
  281. print "Upload: %s" % uploaded_file
  282. print
  283. print "New files:"
  284. lst = fd.list()
  285. print lst
  286. file_id = -1
  287. for file_data in lst:
  288. if file_data[6] == uploaded_file:
  289. file_id = file_data[1]
  290. break
  291. if file_id == -1:
  292. print "Can't find file ID :-("
  293. sys.exit(1)
  294. print
  295. print "Making the file private:"
  296. fd.set_perm(file_id, FD_PERM_PRIVATE)
  297. print fd.list()
  298. print
  299. print "Making the file password-protected:"
  300. fd.set_perm(file_id, FD_PERM_PASSWORD, "passtest")
  301. print fd.list()
  302. print
  303. print "Making the file public again:"
  304. fd.set_perm(file_id, FD_PERM_PUBLIC)
  305. print fd.list()
  306. print
  307. print "Making the file private again:"
  308. fd.set_perm(file_id, FD_PERM_PRIVATE)
  309. print fd.list()
  310. print
  311. print "Deleting the file:"
  312. fd.delete(file_id)
  313. print fd.list()