When saving a note, auto-delete any files that used to be linked from it but no longer are. Still need unit tests.
This commit is contained in:
parent
7b8f6bd6e5
commit
65ce915755
|
@ -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
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import cgi
|
import cgi
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -197,6 +198,8 @@ cherrypy._cpcgifs.FieldStorage = FieldStorage
|
||||||
|
|
||||||
|
|
||||||
class Files( object ):
|
class Files( object ):
|
||||||
|
FILE_LINK_PATTERN = re.compile( u'<a\s+href="[^"]*/files/download\?file_id=([^"]+)">', re.IGNORECASE )
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Controller for dealing with uploaded files, corresponding to the "/files" URL.
|
Controller for dealing with uploaded files, corresponding to the "/files" URL.
|
||||||
"""
|
"""
|
||||||
|
@ -527,3 +530,29 @@ class Files( object ):
|
||||||
self.__database.save( db_file )
|
self.__database.save( db_file )
|
||||||
|
|
||||||
return dict()
|
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()
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Notebooks( object ):
|
||||||
"""
|
"""
|
||||||
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
|
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.
|
Create a new Notebooks object.
|
||||||
|
|
||||||
|
@ -44,11 +44,14 @@ class Notebooks( object ):
|
||||||
@param database: database that notebooks are stored in
|
@param database: database that notebooks are stored in
|
||||||
@type users: controller.Users
|
@type users: controller.Users
|
||||||
@param users: controller for all users, used here for updating storage utilization
|
@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
|
@return: newly constructed Notebooks
|
||||||
"""
|
"""
|
||||||
self.__database = database
|
self.__database = database
|
||||||
self.__users = users
|
self.__users = users
|
||||||
|
self.__files = files
|
||||||
|
|
||||||
@expose( view = Main_page )
|
@expose( view = Main_page )
|
||||||
@strongly_expire
|
@strongly_expire
|
||||||
|
@ -509,6 +512,8 @@ class Notebooks( object ):
|
||||||
|
|
||||||
new_revision = User_revision( note.revision, note.user_id, user.username )
|
new_revision = User_revision( note.revision, note.user_id, user.username )
|
||||||
|
|
||||||
|
self.__files.purge_unused( note )
|
||||||
|
|
||||||
return new_revision
|
return new_revision
|
||||||
|
|
||||||
# if the note is already in the given notebook, load it and update it
|
# if the note is already in the given notebook, load it and update it
|
||||||
|
|
|
@ -43,8 +43,8 @@ class Root( object ):
|
||||||
settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
|
settings[ u"global" ].get( u"luminotes.payment_email", u"" ),
|
||||||
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
|
settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
|
||||||
)
|
)
|
||||||
self.__notebooks = Notebooks( database, self.__users )
|
|
||||||
self.__files = Files( 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
|
self.__suppress_exceptions = suppress_exceptions # used for unit tests
|
||||||
|
|
||||||
@expose( Main_page )
|
@expose( Main_page )
|
||||||
|
|
|
@ -98,6 +98,18 @@ class File( Persistent ):
|
||||||
def sql_delete( self ):
|
def sql_delete( self ):
|
||||||
return "delete from file where id = %s;" % quote( self.object_id )
|
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 ):
|
def to_dict( self ):
|
||||||
d = Persistent.to_dict( self )
|
d = Persistent.to_dict( self )
|
||||||
d.update( dict(
|
d.update( dict(
|
||||||
|
|
|
@ -8,3 +8,4 @@ create table file (
|
||||||
content_type text
|
content_type text
|
||||||
);
|
);
|
||||||
alter table file add primary key ( id );
|
alter table file add primary key ( id );
|
||||||
|
create index file_note_id_index on file using btree ( note_id );
|
||||||
|
|
|
@ -234,6 +234,13 @@ ALTER TABLE ONLY user_notebook
|
||||||
ADD CONSTRAINT user_notebook_pkey PRIMARY KEY (user_id, notebook_id);
|
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:
|
-- Name: luminotes_user_email_address_index; Type: INDEX; Schema: public; Owner: luminotes; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
Reference in New Issue