From eb18b6020dd632ad369c5fcb52185e0239380dee Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 20 Feb 2008 23:25:13 +0000 Subject: [PATCH] File renaming works. Unit tests still pending. File deleting implemented. Testing and unit tests still pending. Now releasing session lock at top of download() to prevent session deadlocks. --- controller/Files.py | 80 +++++++++++++++++++++++++++++++++++++++++---- model/File.py | 8 ++++- static/js/Wiki.js | 38 ++++++++++++++++++--- 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/controller/Files.py b/controller/Files.py index 163ac8c..10df25d 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -232,6 +232,10 @@ class Files( object ): @return: file data @raise Access_error: the current user doesn't have access to the notebook that the file is in """ + # release the session lock before beginning to stream the download. otherwise, if the + # upload is cancelled before it's done, the lock won't be released + cherrypy.session.release_lock() + db_file = self.__database.load( File, file_id ) if not db_file or not self.__users.check_access( user_id, db_file.notebook_id ): @@ -431,8 +435,8 @@ class Files( object ): ) def stats( self, file_id, user_id = None ): """ - Return information on a file that has been completely uploaded and is stored in the database. - Also return the user's current storage utilization in bytes. + Return information on a file that has been completely uploaded with its metadata stored in the + database. Also return the user's current storage utilization in bytes. @type file_id: unicode @param file_id: id of the file to report on @@ -442,7 +446,7 @@ class Files( object ): @return: { 'filename': filename, 'size_bytes': filesize, - 'storage_bytes': current sturage usage by user + 'storage_bytes': current storage usage by user } @raise Access_error: the current user doesn't have access to the notebook that the file is in """ @@ -461,8 +465,70 @@ class Files( object ): storage_bytes = user.storage_bytes, ) - def delete( self, file_id ): - pass # TODO + @expose( view = Json ) + @grab_user_id + @validate( + file_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def delete( self, file_id, user_id = None ): + """ + Delete a file that has been completely uploaded, removing both its metadata from the database + and its data from the filesystem. Return the user's current storage utilization in bytes. - def rename( self, file_id, filename ): - pass # TODO + @type file_id: unicode + @param file_id: id of the file to delete + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @rtype: dict + @return: { + 'storage_bytes': current storage usage by user + } + @raise Access_error: the current user doesn't have access to the notebook that the file is in + """ + db_file = self.__database.load( File, file_id ) + + if not db_file or not self.__users.check_access( user_id, db_file.notebook_id, read_write = True ): + raise Access_error() + + user = self.__database.load( User, user_id ) + if not user: + raise Access_error() + + self.__database.execute( db_file.sql_delete() ) + os.remove( Upload_file.make_server_filename( file_id ) ) + + return dict( + storage_bytes = user.storage_bytes, + ) + + @expose( view = Json ) + @grab_user_id + @validate( + file_id = Valid_id(), + filename = unicode, + user_id = Valid_id( none_okay = True ), + ) + def rename( self, file_id, filename, user_id = None ): + """ + Rename a file that has been completely uploaded. + + @type file_id: unicode + @param file_id: id of the file to delete + @type filename: unicode + @param filename: new name for the file + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @rtype: dict + @return: {} + @raise Access_error: the current user doesn't have access to the notebook that the file is in + """ + db_file = self.__database.load( File, file_id ) + + if not db_file or not self.__users.check_access( user_id, db_file.notebook_id, read_write = True ): + raise Access_error() + + db_file.filename = filename + self.__database.save( db_file ) + + return dict() diff --git a/model/File.py b/model/File.py index 29a3c2a..3189eb1 100644 --- a/model/File.py +++ b/model/File.py @@ -95,6 +95,9 @@ class File( Persistent ): ( quote( self.revision ), quote( self.__notebook_id ), quote( self.__note_id ), quote( self.__filename ), self.__size_bytes or 'null', quote( self.__content_type ), quote( self.object_id ) ) + def sql_delete( self ): + return "delete from file where file_id = %s;" % quote( self.object_id ) + def to_dict( self ): d = Persistent.to_dict( self ) d.update( dict( @@ -107,8 +110,11 @@ class File( Persistent ): return d + def __set_filename( self, filename ): + self.__filename = filename + notebook_id = property( lambda self: self.__notebook_id ) note_id = property( lambda self: self.__note_id ) - filename = property( lambda self: self.__filename ) + filename = property( lambda self: self.__filename, __set_filename ) size_bytes = property( lambda self: self.__size_bytes ) content_type = property( lambda self: self.__content_type ) diff --git a/static/js/Wiki.js b/static/js/Wiki.js index a2a1138..0112404 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -2395,27 +2395,40 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) { connect( this.filename_field, "onblur", function ( event ) { self.filename_field_changed( event ); } ); connect( this.filename_field, "onkeydown", function ( event ) { self.filename_field_key_pressed( event ); } ); + var delete_button = createDOM( "input", { + "type": "button", + "class": "button", + "value": "delete", + "title": "delete file" + } ); + appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "filename: " ) ); appendChildNodes( this.div, this.filename_field ); appendChildNodes( this.div, this.file_size ); + appendChildNodes( this.div, " " ); + appendChildNodes( this.div, delete_button ); var query = parse_query( link ); - var file_id = query.file_id; + this.file_id = query.file_id; // get the file's name and size from the server this.invoker.invoke( "/files/stats", "GET", { - "file_id": file_id + "file_id": this.file_id }, function ( result ) { // if the user has already started typing something, don't overwrite it - if ( self.filename_field.value.length == 0 ) + if ( self.filename_field.value.length == 0 ) { self.filename_field.value = result.filename; + self.previous_filename = result.filename; + } replaceChildNodes( self.file_size, bytes_to_megabytes( result.size_bytes, true ) ); self.wiki.display_storage_usage( result.storage_bytes ); } ); + connect( delete_button, "onclick", function ( event ) { self.delete_button_clicked( event ); } ); + // FIXME: when this is called, the text cursor moves to an unexpected location editor.focus(); } @@ -2437,11 +2450,15 @@ File_link_pulldown.prototype.filename_field_changed = function ( event ) { if ( filename == this.previous_filename ) return; + var title = link_title( this.link ); + if ( title == this.previous_filename ) + replaceChildNodes( this.link, this.editor.document.createTextNode( filename ) ); + this.previous_filename = filename; this.invoker.invoke( - "/files/rename", "GET", { - "file_id": file_id, + "/files/rename", "POST", { + "file_id": this.file_id, "filename": filename } ); @@ -2456,6 +2473,17 @@ File_link_pulldown.prototype.filename_field_key_pressed = function ( event ) { } } +File_link_pulldown.prototype.delete_button_clicked = function ( event ) { + var self = this; + + this.invoker.invoke( + "/files/delete", "POST", { + "file_id": this.file_id + }, + function ( result ) { self.wiki.display_storage_usage( result.storage_bytes ); } + ); +} + File_link_pulldown.prototype.update_position = function ( anchor, relative_to ) { Pulldown.prototype.update_position.call( this, anchor, relative_to ); }