diff --git a/controller/Async.py b/controller/Async.py deleted file mode 100644 index 7655c6a..0000000 --- a/controller/Async.py +++ /dev/null @@ -1,16 +0,0 @@ -import cherrypy - - -def async( method ): - """ - A decorator for a generator method that causes it to be invoked asynchronously. In other words, - whenever a generator method decorated by this decorator is called, its generator is added to - the scheduler for later execution. - - This decorator expects a self.scheduler member containing the scheduler to use. - """ - def schedule( self, *args, **kwargs ): - thread = method( self, *args, **kwargs ) - self.scheduler.add( thread ) - - return schedule diff --git a/controller/Files.py b/controller/Files.py index bb6828f..ed817d4 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -1,4 +1,5 @@ import os +import re import cgi import time import tempfile @@ -197,6 +198,8 @@ cherrypy._cpcgifs.FieldStorage = FieldStorage class Files( object ): + FILE_LINK_PATTERN = re.compile( u'', re.IGNORECASE ) + """ Controller for dealing with uploaded files, corresponding to the "/files" URL. """ @@ -527,3 +530,29 @@ class Files( object ): self.__database.save( db_file ) return dict() + + def purge_unused( self, note ): + """ + Delete files that were linked from the given note but no longer are. + + @type note: model.Note + @param note: note to search for file links + """ + # load metadata for all files with the given note's note_id + files = self.__database.select_many( File, File.sql_load_note_files( note.object_id ) ) + files_to_delete = dict( [ ( db_file.object_id, db_file ) for db_file in files ] ) + + # search through the note's contents for current links to files + for match in self.FILE_LINK_PATTERN.finditer( note.contents ): + file_id = match.groups( 0 )[ 0 ] + + # we've found a link for file_id, so don't delete that file + files_to_delete.pop( file_id, None ) + + # for each file to delete, delete its metadata from the database and its data from the + # filesystem + for ( file_id, db_file ) in files_to_delete.items(): + self.__database.execute( db_file.sql_delete(), commit = False ) + os.remove( Upload_file.make_server_filename( file_id ) ) + + self.__database.commit() diff --git a/controller/Notebooks.py b/controller/Notebooks.py index ea21ff5..46d46f3 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -36,7 +36,7 @@ class Notebooks( object ): """ Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL. """ - def __init__( self, database, users ): + def __init__( self, database, users, files ): """ Create a new Notebooks object. @@ -44,11 +44,14 @@ class Notebooks( object ): @param database: database that notebooks are stored in @type users: controller.Users @param users: controller for all users, used here for updating storage utilization - @rtype: Notebooks + @type files: controller.Files + @param files: controller for all uploaded files, used here for deleting files that are no longer + referenced within saved notes @return: newly constructed Notebooks """ self.__database = database self.__users = users + self.__files = files @expose( view = Main_page ) @strongly_expire @@ -509,6 +512,8 @@ class Notebooks( object ): new_revision = User_revision( note.revision, note.user_id, user.username ) + self.__files.purge_unused( note ) + return new_revision # if the note is already in the given notebook, load it and update it diff --git a/controller/Root.py b/controller/Root.py index e419966..d3f8ee8 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -43,8 +43,8 @@ class Root( object ): settings[ u"global" ].get( u"luminotes.payment_email", u"" ), settings[ u"global" ].get( u"luminotes.rate_plans", [] ), ) - self.__notebooks = Notebooks( database, self.__users ) self.__files = Files( database, self.__users ) + self.__notebooks = Notebooks( database, self.__users, self.__files ) self.__suppress_exceptions = suppress_exceptions # used for unit tests @expose( Main_page ) diff --git a/model/File.py b/model/File.py index 3f620be..c8df6d6 100644 --- a/model/File.py +++ b/model/File.py @@ -98,6 +98,18 @@ class File( Persistent ): def sql_delete( self ): return "delete from file where id = %s;" % quote( self.object_id ) + @staticmethod + def sql_load_note_files( note_id ): + return \ + """ + select + file.id, file.revision, file.notebook_id, file.note_id, file.filename, file.size_bytes, file.content_type + from + file + where + file.note_id = %s; + """ % quote( note_id ) + def to_dict( self ): d = Persistent.to_dict( self ) d.update( dict( diff --git a/model/delta/1.2.0.sql b/model/delta/1.2.0.sql index c2b46c9..5a2ad05 100644 --- a/model/delta/1.2.0.sql +++ b/model/delta/1.2.0.sql @@ -8,3 +8,4 @@ create table file ( content_type text ); alter table file add primary key ( id ); +create index file_note_id_index on file using btree ( note_id ); diff --git a/model/schema.sql b/model/schema.sql index a72c9e2..3cc08b6 100644 --- a/model/schema.sql +++ b/model/schema.sql @@ -234,6 +234,13 @@ ALTER TABLE ONLY user_notebook ADD CONSTRAINT user_notebook_pkey PRIMARY KEY (user_id, notebook_id); +-- +-- Name: file_note_id_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: +-- + +CREATE INDEX file_note_id_index ON file USING btree (note_id); + + -- -- Name: luminotes_user_email_address_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace: --