def handle_put(reqinfo, start_response): '''Write to a single file, possibly replacing an existing one.''' real_path = reqinfo.get_request_path('w') if os.path.isdir(real_path): raise DAVError('405 Method Not Allowed: Overwriting directory') if os.path.exists(real_path): etag = davutils.create_etag(real_path) else: etag = None if not reqinfo.check_ifmatch(etag): raise DAVError('412 Precondition Failed') new_file = not os.path.exists(real_path) if not new_file: # Unlink the old file to reset mode bits. # This has the additional benefit that old GET operations can # continue even if the file is replaced. os.unlink(real_path) outfile = open(real_path, 'wb') block_generator = davutils.read_blocks(reqinfo.wsgi_input) davutils.write_blocks(outfile, block_generator) if new_file: start_response('201 Created', []) else: start_response('204 No Content', []) return ""
def _sql_query(self, *args, **kwargs): '''Run a database query and wrap SQLite OperationalErrors, such as locked databases. ''' try: self.db_cursor.execute(*args, **kwargs) except sqlite3.OperationalError, e: if 'locked' in e.message: raise DAVError('503 Service Unavailable: Lock DB is busy') else: raise DAVError('500 Internal Server Error: Lock DB: ' + e.message)
def handle_lock(reqinfo, start_response): '''Create a lock or refresh an existing one.''' timeout = reqinfo.get_timeout() depth = reqinfo.get_depth() real_path = reqinfo.get_request_path('wl') rel_path = davutils.get_relpath(real_path, config.root_dir) if not reqinfo.lockmanager: raise DAVError('501 Not Implemented: Lock support disabled') if not reqinfo.length: # Handle lock refresh lock = reqinfo.lockmanager.refresh_lock(rel_path, reqinfo.provided_tokens[0][1], timeout) else: # Create new lock shared, owner = reqinfo.parse_lock_body() lock = reqinfo.lockmanager.create_lock(rel_path, shared, owner, depth, timeout) if not os.path.exists(real_path): status = "201 Created" open(real_path, 'w').write('') else: status = "200 OK" start_response(status, [('Content-Type', 'text/xml; charset=utf-8'), ('Lock-Token', lock.urn)]) t = activelock.Template(lock=lock, reqinfo=reqinfo, part_only=False) return [t.serialize(output='xml')]
def handle_copy_move(reqinfo, start_response): '''Copy or move a file or a directory.''' reqinfo.assert_nobody() depth = reqinfo.get_depth() real_source = reqinfo.get_request_path('r') real_dest = reqinfo.get_destination_path('w') new_resource = not os.path.exists(real_dest) if not new_resource: if not reqinfo.get_overwrite(): raise DAVError('412 Precondition Failed: Would overwrite') elif os.path.isdir(real_dest): shutil.rmtree(real_dest) else: os.unlink(real_dest) if reqinfo.environ['REQUEST_METHOD'] == 'COPY': if os.path.isdir(real_source): if depth == 0: os.mkdir(real_dest) shutil.copystat(real_source, real_dest) else: shutil.copytree(real_source, real_dest, symlinks=True) else: shutil.copy2(real_source, real_dest) else: real_source = reqinfo.get_request_path('wd') shutil.move(real_source, real_dest) purge_locks(reqinfo.lockmanager, real_source) if new_resource: start_response('201 Created', []) else: start_response('204 No Content', []) return ""
def main(environ, start_response): '''Main WSGI program to handle requests. Calls handlers from request_handlers. ''' try: logging.info( environ.get('REMOTE_ADDR') + ' ' + environ.get('REQUEST_METHOD') + ' ' + environ.get('PATH_INFO')) request_method = environ.get('REQUEST_METHOD', '').upper() if environ.get('HTTP_EXPECT') and __name__ == '__main__': # Expect should work with fcgi etc., but not with the simple_server # that is used for testing. start_response('400 Bad Request: Expect not supported', []) return "" environ['wsgi.input'] = WSGIInputWrapper(environ) try: reqinfo = RequestInfo(environ) if request_handlers.has_key(request_method): return request_handlers[request_method](reqinfo, start_response) else: raise DAVError('501 Not Implemented') except DAVError, e: environ['wsgi.input'].read() # Discard request body if not e.body: logging.warn(e.httpstatus) start_response(e.httpstatus, [('Content-Type', 'text/plain')]) return [e.httpstatus] else: logging.warn(e.httpstatus + ' ' + e.body) start_response(e.httpstatus, [('Content-Type', 'text/xml')]) return [e.body] except: import traceback exc = traceback.format_exc() logging.error('Request handler crashed', exc_info=1) if isinstance(environ['wsgi.input'], WSGIInputWrapper): environ['wsgi.input'].read() try: start_response('500 Internal Server Error', [('Content-Type', 'text/plain')]) except AssertionError: # Ignore duplicate start_response pass return [exc]
def handle_unlock(reqinfo, start_response): '''Remove an existing lock.''' real_path = reqinfo.get_request_path('r') rel_path = davutils.get_relpath(real_path, config.root_dir) urn = reqinfo.environ.get('HTTP_LOCK_TOKEN', '').strip(' <>') if not reqinfo.lockmanager: raise DAVError('501 Not implemented: Lock support disabled') reqinfo.lockmanager.release_lock(rel_path, urn) start_response('204 No Content', []) return ""
def handle_mkcol(reqinfo, start_response): '''Create a new directory.''' reqinfo.assert_nobody() real_path = reqinfo.get_request_path('w') if os.path.exists(real_path): raise DAVError('405 Method Not Allowed: Collection already exists') os.mkdir(real_path) start_response('201 Created', []) return ""
def proppatch_verify_instruction(real_path, instruction): '''Verify that the property can be set on the file, or throw a DAVError. Used to verify instructions before they are executed. ''' command, propname, propelement = instruction if command == 'set': if propelement.getchildren(): raise DAVError( '409 Conflict: XML property values are not supported') if not property_handlers.has_key(propname): raise DAVError('403 Forbidden: No such property') if property_handlers[propname][1] is None: raise DAVError('403 Forbidden', '<DAV:cannot-modify-protected-property/>') elif command == 'remove': # No properties to remove so far. raise DAVError('403 Forbidden: Properties cannot be removed')
def release_lock(self, rel_path, urn): '''Remove a lock from database. The rel_path must match a lock with the specified urn. ''' self._sql_query('BEGIN IMMEDIATE TRANSACTION') try: if not self.validate_lock(rel_path, urn): raise DAVError('409 Conflict', '<DAV:lock-token-matches-request-uri/>') self._sql_query('DELETE FROM locks WHERE urn=?', (urn, )) self._sql_query('END TRANSACTION') except: self._sql_query('ROLLBACK') raise
def handle_delete(reqinfo, start_response): '''Delete a file or a directory.''' reqinfo.assert_nobody() real_path = reqinfo.get_request_path('wd') # Locks on parent directory prohibit deletion of members. reqinfo.assert_locks(os.path.dirname(real_path)) if not os.path.exists(real_path): raise DAVError('404 Not Found') if os.path.isdir(real_path): shutil.rmtree(real_path) else: os.unlink(real_path) purge_locks(reqinfo.lockmanager, real_path) start_response('204 No Content', []) return ""
def refresh_lock(self, rel_path, urn, timeout): '''Refresh the given lock and return new Lock object.''' timeout = min(timeout, config.lock_max_time) or config.lock_max_time valid_until = datetime.datetime.utcnow() valid_until += datetime.timedelta(seconds=timeout) self._sql_query('BEGIN IMMEDIATE TRANSACTION') try: if not self.validate_lock(rel_path, urn): raise DAVError('412 Precondition Failed', '<DAV:lock-token-matches-request-uri/>') self._sql_query('UPDATE locks SET valid_until=? WHERE urn=?', (valid_until, urn)) self._sql_query('END TRANSACTION') except: self._sql_query('ROLLBACK') raise self._sql_query('SELECT * FROM locks WHERE urn=?', (urn, )) return Lock(self.db_cursor.fetchone())
def create_lock(self, rel_path, shared, owner, depth, timeout): '''Create a lock for the resource defined by rel_path. Arguments are as follows: rel_path: full path to the resource in local file system shared: True for shared lock, False for exclusive lock owner: client-provided <DAV:owner> xml string describing the owner of the lock depth: -1 for infinite, 0 otherwise timeout: Client-requested lock expiration time in seconds from now. Configuration may limit actual timeout. Returns a Lock object. ''' assert depth in [-1, 0] assert not rel_path.startswith('/') urn = uuid4().urn timeout = min(timeout, config.lock_max_time) or config.lock_max_time valid_until = datetime.datetime.utcnow() valid_until += datetime.timedelta(seconds=timeout) self._sql_query('BEGIN IMMEDIATE TRANSACTION') try: for lock in self.get_locks(rel_path, depth == -1): if not lock.shared or not shared: # Allow only one exclusive lock raise DAVError('423 Locked') self._sql_query( 'INSERT INTO locks VALUES (?,?,?,?,?,?)', (urn, rel_path, bool(shared), owner, depth == -1, valid_until)) self._sql_query('END TRANSACTION') except: self._sql_query('ROLLBACK') raise self._sql_query('SELECT * FROM locks WHERE urn=?', (urn, )) return Lock(self.db_cursor.fetchone())
def handle_get(reqinfo, start_response): '''Download a single file or show directory index.''' reqinfo.assert_nobody() real_path = reqinfo.get_request_path('r') if os.path.isdir(real_path): return handle_dirindex(reqinfo, start_response) etag = davutils.create_etag(real_path) if not reqinfo.check_ifmatch(etag): raise DAVError('412 Precondition Failed') start_response('200 OK', [ ('Content-Type', davutils.get_mimetype(real_path)), ('Etag', etag), ('Content-Length', str(os.path.getsize(real_path))), ('Last-Modified', davutils.get_rfcformat(os.path.getmtime(real_path))) ]) if reqinfo.environ['REQUEST_METHOD'] == 'HEAD': return '' infile = open(real_path, 'rb') return davutils.read_blocks(infile)
def handle_post(reqinfo, start_response): '''Handle a POST request. Used for file uploads and deletes in the HTML GUI. ''' if 'w' not in config.html_interface: raise DAVError('403 HTML interface is configured as read-only') fields = cgi.FieldStorage(fp=reqinfo.wsgi_input, environ=reqinfo.environ) real_path = reqinfo.get_request_path('r') message = "" if fields.getfirst('file'): f = fields['file'] dest_path = os.path.join(real_path, f.filename) reqinfo.assert_write(dest_path) if os.path.isdir(dest_path): raise DAVError('405 Method Not Allowed: Overwriting directory') if os.path.exists(dest_path): os.unlink(dest_path) outfile = open(dest_path, 'wb') davutils.write_blocks(outfile, davutils.read_blocks(f.file)) message = "Successfully uploaded " + f.filename + "." if fields.getfirst('btn_remove'): filenames = fields.getlist('select') for f in filenames: rm_path = os.path.join(real_path, f) reqinfo.assert_write(rm_path) if os.path.isdir(rm_path): shutil.rmtree(rm_path) else: os.unlink(rm_path) message = "Successfully removed " + str(len(filenames)) + " files." if fields.getfirst('btn_download'): filenames = fields.getlist('select') datafile = tempfile.TemporaryFile() zipobj = zipfile.ZipFile(datafile, 'w', zipfile.ZIP_DEFLATED, True) def check_read(path): '''Callback function for zipping to verify that each file in the zip has access rights.''' try: reqinfo.assert_read(path) return True except DAVError: return False for f in filenames: file_path = os.path.join(real_path, f) reqinfo.assert_read(file_path) davutils.add_to_zip_recursively(zipobj, file_path, config.root_dir, check_read) zipobj.close() start_response('200 OK', [('Content-Type', 'application/zip'), ('Content-Length', str(datafile.tell()))]) datafile.seek(0) return davutils.read_blocks(datafile) return handle_dirindex(reqinfo, start_response, message)