1 # Module: compositefile.py
2 # Author: Eric von Bayer
4 # Date: August 18, 2009
6 # Class that works like a file but is in reality a series of file system
7 # files. Intentionally is a similar API to the file object.
9 # Copyright (c) 2009, Eric von Bayer
10 # All rights reserved.
12 # Redistribution and use in source and binary forms, with or without
13 # modification, are permitted provided that the following conditions are met:
15 # * Redistributions of source code must retain the above copyright notice,
16 # this list of conditions and the following disclaimer.
17 # * Redistributions in binary form must reproduce the above copyright notice,
18 # this list of conditions and the following disclaimer in the documentation
19 # and/or other materials provided with the distribution.
20 # * The names of the contributors may not be used to endorse or promote
21 # products derived from this software without specific prior written
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 except AttributeError:
41 os
.SEEK_SET
, os
.SEEK_CUR
, os
.SEEK_END
= range(3)
43 # Handle a series of files as if it's one file
44 class CompositeFile(object):
45 """Virtual file that is a composite of other files"""
46 def __init__( self
, *files
):
47 self
.__file
_map
= list()
50 # If we are given a composite file, copy the file map
51 if len(files
) == 1 and isinstance( files
[0], CompositeFile
):
52 self
.__file
_map
= copy
.deepcopy( files
[0].__file
_map
)
54 # Build a file offset map
58 size
= os
.path
.getsize( cfile
)
59 if size
> 0 and os
.path
.isfile( cfile
):
61 self
.__file
_map
.append( ( off
, cfile
) )
67 return ",".join( fm
[1] for fm
in self
.__file
_map
)
69 # Advance to the next file
70 def __next_file( self
):
71 # If we have an open handle, close it in preparation for the next one
72 if self
.__handle
!= None and not self
.__handle
.closed
:
75 # Bump the file number that we're on
78 # If there's another file, open it and get the next offset
79 if self
.__fileno
< len( self
.__file
_map
):
80 self
.__handle
= open( self
.__file
_map
[ self
.__fileno
][1], "rb" )
81 self
.__next
_file
_off
= self
.__file
_map
[ self
.__fileno
][0]
83 # We're done, null the handle and mark as closed
87 # Read a slice, return the data and how many bytes remain to be read
88 def __read_slice( self
, bytes
):
89 rem
= self
.__off
+ bytes
- self
.__next
_file
_off
91 # If the remaining bytes aren't negative, then read a slice and advance
95 #print "Read %d[%d]: %d: %s+%d" % ( bytes, rem, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() )
96 data
= self
.__handle
.read( bytes
)
97 self
.__off
+= len(data
)
98 assert bytes
== len(data
), "Failed to read the requested number of bytes"
102 # Read the bytes all from this file
104 #print "Read %d: %d: %s+%d" % ( bytes, self.__off, os.path.basename(self.__handle.name), self.__handle.tell() )
105 data
= self
.__handle
.read( bytes
)
106 self
.__off
+= len(data
)
107 assert bytes
== len(data
), "Failed to read the requested number of bytes"
110 # Get a list of files
112 return [ fm
[1] for fm
in self
.__file
_map
]
114 # Return the linear offset
118 # Seek into the composite file
119 def seek( self
, off
, whence
= os
.SEEK_SET
):
121 # Calculate the new seek offset
122 if whence
== os
.SEEK_SET
:
124 elif whence
== os
.SEEK_CUR
:
125 new_off
= self
.__off
+ off
126 elif whence
== os
.SEEK_END
:
127 new_off
= self
.__file
_map
[-1][0] + off
129 raise "seek called with an invalid offset type"
131 # Determine which file this seek offset is part of
134 for ( eoff
, mfile
) in self
.__file
_map
:
140 # Make sure this was a valid seek point
142 raise "seek beyond the bounds of the composite file"
144 # Make sure the correct file is open
145 if fileno
!= self
.__fileno
:
146 if not self
.__handle
.closed
:
147 self
.__handle
.close()
148 self
.__handle
= open( self
.__file
_map
[fileno
][1], "rb" )
149 self
.__next
_file
_off
= eoff
150 self
.__fileno
= fileno
152 # Seek the file handle
153 self
.__handle
.seek( new_off
- soff
)
157 def read( self
, bytes
):
158 if self
.__handle
.closed
== False:
160 while bytes
> 0 and self
.closed
== False:
161 slice_data
, bytes
= self
.__read
_slice
( bytes
)
171 self
.__next
_file
_off
= 0L
175 # Open the first file
180 if self
.__handle
!= None and self
.__handle
.closed
== False:
181 self
.__handle
.close()
186 return self
.__file
_map
[-1][0]