@@ -147,8 +147,7 @@ class LockFile(object):
147147
148148 As we are a utility class to be derived from, we only use protected methods.
149149
150- Locks will automatically be released on destruction
151- """
150+ Locks will automatically be released on destruction """
152151 __slots__ = ("_file_path" , "_owns_lock" )
153152
154153 def __init__ (self , file_path ):
@@ -216,8 +215,10 @@ def _release_lock(self):
216215 # if someone removed our file beforhand, lets just flag this issue
217216 # instead of failing, to make it more usable.
218217 lfp = self ._lock_file_path ()
219- if os . path . isfile ( lfp ) :
218+ try :
220219 os .remove (lfp )
220+ except OSError :
221+ pass
221222 self ._owns_lock = False
222223
223224
@@ -271,86 +272,144 @@ def _obtain_lock(self):
271272 # END endless loop
272273
273274
274- class ConcurrentWriteOperation (LockFile ):
275- """
276- This class facilitates a safe write operation to a file on disk such that we:
275+ class FDStreamWrapper (object ):
276+ """A simple wrapper providing the most basic functions on a file descriptor
277+ with the fileobject interface. Cannot use os.fdopen as the resulting stream
278+ takes ownership"""
279+ __slots__ = ("_fd" , '_pos' )
280+ def __init__ (self , fd ):
281+ self ._fd = fd
282+ self ._pos = 0
283+
284+ def write (self , data ):
285+ self ._pos += len (data )
286+ os .write (self ._fd , data )
277287
278- - lock the original file
279- - write to a temporary file
280- - rename temporary file back to the original one on close
281- - unlock the original file
288+ def read (self , count = 0 ):
289+ if count == 0 :
290+ count = os .path .getsize (self ._filepath )
291+ # END handle read everything
292+
293+ bytes = os .read (self ._fd , count )
294+ self ._pos += len (bytes )
295+ return bytes
282296
297+ def fileno (self ):
298+ return self ._fd
299+
300+ def tell (self ):
301+ return self ._pos
302+
303+
304+ class LockedFD (LockFile ):
305+ """This class facilitates a safe read and write operation to a file on disk.
306+ If we write to 'file', we obtain a lock file at 'file.lock' and write to
307+ that instead. If we succeed, the lock file will be renamed to overwrite
308+ the original file.
309+
310+ When reading, we obtain a lock file, but to prevent other writers from
311+ succeeding while we are reading the file.
312+
283313 This type handles error correctly in that it will assure a consistent state
284- on destruction
285- """
286- __slots__ = "_temp_write_fp"
314+ on destruction.
287315
288- def __init__ (self , file_path ):
289- """
290- Initialize an instance able to write the given file_path
291- """
292- super (ConcurrentWriteOperation , self ).__init__ (file_path )
293- self ._temp_write_fp = None
316+ :note: with this setup, parallel reading is not possible"""
317+ __slots__ = ("_filepath" , '_fd' , '_write' )
318+
319+ def __init__ (self , filepath ):
320+ """Initialize an instance with the givne filepath"""
321+ self ._filepath = filepath
322+ self ._fd = None
323+ self ._write = None # if True, we write a file
294324
295325 def __del__ (self ):
296- self ._end_writing (successful = False )
326+ # will do nothing if the file descriptor is already closed
327+ if self ._fd is not None :
328+ self .rollback ()
297329
298- def _begin_writing (self ):
299- """
300- Begin writing our file, hence we get a lock and start writing
301- a temporary file in the same directory.
330+ def _lockfilepath (self ):
331+ return "%s.lock" % self ._filepath
302332
303- Returns
304- File Object to write to. It is still maintained by this instance
305- and you do not need to manually close
306- """
307- # already writing ?
308- if self ._temp_write_fp is not None :
309- return self ._temp_write_fp
310-
311- self ._obtain_lock_or_raise ()
312- dirname , basename = os .path .split (self ._file_path )
313- self ._temp_write_fp = open (tempfile .mktemp (basename , '' , dirname ), "wb" )
314- return self ._temp_write_fp
333+ def open (self , write = False , stream = False ):
334+ """Open the file descriptor for reading or writing, both in binary mode.
335+ :param write: if True, the file descriptor will be opened for writing. Other
336+ wise it will be opened read-only.
337+ :param stream: if True, the file descriptor will be wrapped into a simple stream
338+ object which supports only reading or writing
339+ :return: fd to read from or write to. It is still maintained by this instance
340+ and must not be closed directly
341+ :raise IOError: if the lock could not be retrieved
342+ :raise OSError: If the actual file could not be opened for reading
343+ :note: must only be called once"""
344+ if self ._write is not None :
345+ raise AssertionError ("Called %s multiple times" % self .open )
346+
347+ self ._write = write
348+
349+ # try to open the lock file
350+ binary = getattr (os , 'O_BINARY' , 0 )
351+ lockmode = os .O_WRONLY | os .O_CREAT | os .O_EXCL | binary
352+ try :
353+ fd = os .open (self ._lockfilepath (), lockmode )
354+ if not write :
355+ os .close (fd )
356+ else :
357+ self ._fd = fd
358+ # END handle file descriptor
359+ except OSError :
360+ raise IOError ("Lock at %r could not be obtained" % self ._lockfilepath ())
361+ # END handle lock retrieval
362+
363+ # open actual file if required
364+ if self ._fd is None :
365+ # we could specify exlusive here, as we obtained the lock anyway
366+ self ._fd = os .open (self ._filepath , os .O_RDONLY | binary )
367+ # END open descriptor for reading
368+
369+ if stream :
370+ return FDStreamWrapper (self ._fd )
371+ else :
372+ return self ._fd
373+ # END handle stream
374+
375+ def commit (self ):
376+ """When done writing, call this function to commit your changes into the
377+ actual file.
378+ The file descriptor will be closed, and the lockfile handled.
379+ :note: can be called multiple times"""
380+ self ._end_writing (successful = True )
381+
382+ def rollback (self ):
383+ """Abort your operation without any changes. The file descriptor will be
384+ closed, and the lock released.
385+ :note: can be called multiple times"""
386+ self ._end_writing (successful = False )
315387
316- def _is_writing (self ):
317- """
318- Returns
319- True if we are currently writing a file
320- """
321- return self ._temp_write_fp is not None
322-
323388 def _end_writing (self , successful = True ):
324- """
325- Indicate you successfully finished writing the file to:
389+ """Handle the lock according to the write mode """
390+ if self ._write is None :
391+ raise AssertionError ("Cannot end operation if it wasn't started yet" )
326392
327- - close the underlying stream
328- - rename the remporary file to the original one
329- - release our lock
330- """
331- # did we start a write operation ?
332- if self ._temp_write_fp is None :
333- return
334-
335- if not self ._temp_write_fp .closed :
336- self ._temp_write_fp .close ()
393+ if self ._fd is None :
394+ return
337395
338- if successful :
396+ os .close (self ._fd )
397+ self ._fd = None
398+
399+ lockfile = self ._lockfilepath ()
400+ if self ._write and successful :
339401 # on windows, rename does not silently overwrite the existing one
340402 if sys .platform == "win32" :
341- if os .path .isfile (self ._file_path ):
342- os .remove (self ._file_path )
403+ if os .path .isfile (self ._filepath ):
404+ os .remove (self ._filepath )
343405 # END remove if exists
344406 # END win32 special handling
345- os .rename (self . _temp_write_fp . name , self ._file_path )
407+ os .rename (lockfile , self ._filepath )
346408 else :
347409 # just delete the file so far, we failed
348- os .remove (self . _temp_write_fp . name )
410+ os .remove (lockfile )
349411 # END successful handling
350412
351- # finally reset our handle
352- self ._release_lock ()
353- self ._temp_write_fp = None
354413
355414
356415class LazyMixin (object ):
0 commit comments