Back to index

moin  1.9.0~rc2
filesys.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - File System Utilities
00004 
00005     @copyright: 2002 Juergen Hermann <jh@web.de>,
00006                 2006-2008 MoinMoin:ThomasWaldmann
00007     @license: GNU GPL, see COPYING for details.
00008 """
00009 
00010 import sys, os, shutil, time, errno, random
00011 from stat import S_ISDIR, ST_MODE, S_IMODE
00012 
00013 from MoinMoin import log
00014 logging = log.getLogger(__name__)
00015 
00016 #############################################################################
00017 ### Misc Helpers
00018 #############################################################################
00019 
00020 def chmod(name, mode, catchexception=True):
00021     """ change mode of some file/dir on platforms that support it.
00022         usually you don't need this because we use os.umask() when importing
00023         request.py
00024     """
00025     try:
00026         os.chmod(name, mode)
00027     except OSError:
00028         if not catchexception:
00029             raise
00030 
00031 
00032 def rename(oldname, newname):
00033     """ Multiplatform rename
00034 
00035     Needed because win32 rename is not POSIX compliant, and does not
00036     remove target file if it exists.
00037 
00038     Problem: this "rename" is not atomic any more on win32.
00039 
00040     FIXME: What about rename locking? we can have a lock file in the
00041     page directory, named: PageName.lock, and lock this file before we
00042     rename, then unlock when finished.
00043     """
00044     if os.name == 'nt':
00045         # Windows "rename" taken from Mercurial's util.py. Thanks!
00046         try:
00047             os.rename(oldname, newname)
00048         except OSError, err:
00049             # On windows, rename to existing file is not allowed, so we
00050             # must delete destination first. But if a file is open, unlink
00051             # schedules it for delete but does not delete it. Rename
00052             # happens immediately even for open files, so we rename
00053             # destination to a temporary name, then delete that. Then
00054             # rename is safe to do.
00055             # The temporary name is chosen at random to avoid the situation
00056             # where a file is left lying around from a previous aborted run.
00057             # The usual race condition this introduces can't be avoided as
00058             # we need the name to rename into, and not the file itself. Due
00059             # to the nature of the operation however, any races will at worst
00060             # lead to the rename failing and the current operation aborting.
00061 
00062             if err.errno != errno.EEXIST:
00063                 raise
00064 
00065             def tempname(prefix):
00066                 for tries in xrange(10):
00067                     temp = '%s-%08x' % (prefix, random.randint(0, 0xffffffff))
00068                     if not os.path.exists(temp):
00069                         return temp
00070                 raise IOError, (errno.EEXIST, "No usable temporary filename found")
00071 
00072             temp = tempname(newname)
00073             os.rename(newname, temp)
00074             os.unlink(temp)
00075             os.rename(oldname, newname)
00076     else:
00077         # POSIX: just do it :)
00078         os.rename(oldname, newname)
00079 
00080 rename_overwrite = rename
00081 
00082 def rename_no_overwrite(oldname, newname, delete_old=False):
00083     """ Multiplatform rename
00084 
00085     This kind of rename is doing things differently: it fails if newname
00086     already exists. This is the usual thing on win32, but not on posix.
00087 
00088     If delete_old is True, oldname is removed in any case (even if the
00089     rename did not succeed).
00090     """
00091     if os.name == 'nt':
00092         try:
00093             try:
00094                 os.rename(oldname, newname)
00095                 success = True
00096             except:
00097                 success = False
00098                 raise
00099         finally:
00100             if not success and delete_old:
00101                 os.unlink(oldname)
00102     else:
00103         try:
00104             try:
00105                 os.link(oldname, newname)
00106                 success = True
00107             except:
00108                 success = False
00109                 raise
00110         finally:
00111             if success or delete_old:
00112                 os.unlink(oldname)
00113 
00114 
00115 def touch(name):
00116     if sys.platform == 'win32':
00117         import win32file, win32con, pywintypes
00118 
00119         access = win32file.GENERIC_WRITE
00120         share = (win32file.FILE_SHARE_DELETE |
00121                  win32file.FILE_SHARE_READ |
00122                  win32file.FILE_SHARE_WRITE)
00123         create = win32file.OPEN_EXISTING
00124         mtime = time.gmtime()
00125         handle = win32file.CreateFile(name, access, share, None, create,
00126                                       win32file.FILE_ATTRIBUTE_NORMAL |
00127                                       win32con.FILE_FLAG_BACKUP_SEMANTICS,
00128                                       None)
00129         try:
00130             newTime = pywintypes.Time(mtime)
00131             win32file.SetFileTime(handle, newTime, newTime, newTime)
00132         finally:
00133             win32file.CloseHandle(handle)
00134     else:
00135         os.utime(name, None)
00136 
00137 
00138 def access_denied_decorator(fn):
00139     """ Due to unknown reasons, some os.* functions on Win32 sometimes fail
00140         with Access Denied (although access should be possible).
00141         Just retrying it a bit later works and this is what we do.
00142     """
00143     if sys.platform == 'win32':
00144         def wrapper(*args, **kwargs):
00145             max_retries = 42
00146             retry = 0
00147             while True:
00148                 try:
00149                     return fn(*args, **kwargs)
00150                 except OSError, err:
00151                     retry += 1
00152                     if retry > max_retries:
00153                         raise
00154                     if err.errno == errno.EACCES:
00155                         logging.warning('%s(%r, %r) -> access denied. retrying...' % (fn.__name__, args, kwargs))
00156                         time.sleep(0.01)
00157                         continue
00158                     raise
00159         return wrapper
00160     else:
00161         return fn
00162 
00163 stat = access_denied_decorator(os.stat)
00164 mkdir = access_denied_decorator(os.mkdir)
00165 rmdir = access_denied_decorator(os.rmdir)
00166 
00167 
00168 def fuid(filename, max_staleness=3600):
00169     """ return a unique id for a file
00170 
00171         Using just the file's mtime to determine if the file has changed is
00172         not reliable - if file updates happen faster than the file system's
00173         mtime granularity, then the modification is not detectable because
00174         the mtime is still the same.
00175 
00176         This function tries to improve by using not only the mtime, but also
00177         other metadata values like file size and inode to improve reliability.
00178 
00179         For the calculation of this value, we of course only want to use data
00180         that we can get rather fast, thus we use file metadata, not file data
00181         (file content).
00182 
00183         Note: depending on the operating system capabilities and the way the
00184               file update is done, this function might return the same value
00185               even if the file has changed. It should be better than just
00186               using file's mtime though.
00187               max_staleness tries to avoid the worst for these cases.
00188 
00189         @param filename: file name of the file to look at
00190         @param max_staleness: if a file is older than that, we may consider
00191                               it stale and return a different uid - this is a
00192                               dirty trick to work around changes never being
00193                               detected. Default is 3600 seconds, use None to
00194                               disable this trickery. See below for more details.
00195         @return: an object that changes value if the file changed,
00196                  None is returned if there were problems accessing the file
00197     """
00198     try:
00199         st = os.stat(filename)
00200     except (IOError, OSError):
00201         uid = None  # for permanent errors on stat() this does not change, but
00202                     # having a changing value would be pointless because if we
00203                     # can't even stat the file, it is unlikely we can read it.
00204     else:
00205         fake_mtime = int(st.st_mtime)
00206         if not st.st_ino and max_staleness:
00207             # st_ino being 0 likely means that we run on a platform not
00208             # supporting it (e.g. win32) - thus we likely need this dirty
00209             # trick
00210             now = int(time.time())
00211             if now >= st.st_mtime + max_staleness:
00212                 # keep same fake_mtime for each max_staleness interval
00213                 fake_mtime = int(now / max_staleness) * max_staleness
00214         uid = (st.st_mtime,  # might have a rather rough granularity, e.g. 2s
00215                              # on FAT, 1s on ext3 and might not change on fast
00216                              # updates
00217                st.st_ino,  # inode number (will change if the update is done
00218                            # by e.g. renaming a temp file to the real file).
00219                            # not supported on win32 (0 ever)
00220                st.st_size,  # likely to change on many updates, but not
00221                             # sufficient alone
00222                fake_mtime,  # trick to workaround file system / platform
00223                             # limitations causing permanent trouble
00224               )
00225     return uid
00226 
00227 
00228 def copystat(src, dst):
00229     """Copy stat bits from src to dst
00230 
00231     This should be used when shutil.copystat would be used on directories
00232     on win32 because win32 does not support utime() for directories.
00233 
00234     According to the official docs written by Microsoft, it returns ENOACCES if the
00235     supplied filename is a directory. Looks like a trainee implemented the function.
00236     """
00237     if sys.platform == 'win32' and S_ISDIR(os.stat(dst)[ST_MODE]):
00238         if os.name == 'nt':
00239             st = os.stat(src)
00240             mode = S_IMODE(st[ST_MODE])
00241             if hasattr(os, 'chmod'):
00242                 os.chmod(dst, mode) # KEEP THIS ONE!
00243         #else: pass # we are on Win9x,ME - no chmod here
00244     else:
00245         shutil.copystat(src, dst)
00246 
00247 
00248 def copytree(src, dst, symlinks=False):
00249     """Recursively copy a directory tree using copy2().
00250 
00251     The destination directory must not already exist.
00252     If exception(s) occur, an Error is raised with a list of reasons.
00253 
00254     If the optional symlinks flag is true, symbolic links in the
00255     source tree result in symbolic links in the destination tree; if
00256     it is false, the contents of the files pointed to by symbolic
00257     links are copied.
00258 
00259     In contrary to shutil.copytree, this version also copies directory
00260     stats, not only file stats.
00261 
00262     """
00263     names = os.listdir(src)
00264     os.mkdir(dst)
00265     copystat(src, dst)
00266     errors = []
00267     for name in names:
00268         srcname = os.path.join(src, name)
00269         dstname = os.path.join(dst, name)
00270         try:
00271             if symlinks and os.path.islink(srcname):
00272                 linkto = os.readlink(srcname)
00273                 os.symlink(linkto, dstname)
00274             elif os.path.isdir(srcname):
00275                 copytree(srcname, dstname, symlinks)
00276             else:
00277                 shutil.copy2(srcname, dstname)
00278             # XXX What about devices, sockets etc.?
00279         except (IOError, os.error), why:
00280             errors.append((srcname, dstname, why))
00281     if errors:
00282         raise EnvironmentError, errors
00283 
00284 # Code could come from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203
00285 
00286 # we currently do not support locking
00287 LOCK_EX = LOCK_SH = LOCK_NB = 0
00288 
00289 def lock(file, flags):
00290     raise NotImplementedError
00291 
00292 def unlock(file):
00293     raise NotImplementedError
00294 
00295 
00296 # ----------------------------------------------------------------------
00297 # Get real case of path name on case insensitive file systems
00298 # TODO: nt version?
00299 
00300 if sys.platform == 'darwin':
00301 
00302     def realPathCase(path):
00303         """ Return the real case of path e.g. PageName for pagename
00304 
00305         HFS and HFS+ file systems, are case preserving but case
00306         insensitive. You can't have 'file' and 'File' in the same
00307         directory, but you can get the real name of 'file'.
00308 
00309         @param path: string
00310         @rtype: string
00311         @return the real case of path or None
00312         """
00313         try:
00314             from Carbon import File
00315             try:
00316                 return File.FSRef(path).as_pathname()
00317             except File.Error:
00318                 return None
00319         except ImportError:
00320             return None
00321 
00322 else:
00323 
00324     def realPathCase(path):
00325         return None
00326 
00327 # dircache stuff seems to be broken on win32 (at least for FAT32, maybe NTFS)
00328 DCENABLED = 1 # set to 0 to disable dirchache usage
00329 def dcdisable():
00330     global DCENABLED
00331     DCENABLED = 0
00332 
00333 import dircache
00334 
00335 def dclistdir(path):
00336     if sys.platform == 'win32' or not DCENABLED:
00337         return os.listdir(path)
00338     else:
00339         return dircache.listdir(path)
00340 
00341 def dcreset():
00342     if sys.platform == 'win32' or not DCENABLED:
00343         return
00344     else:
00345         return dircache.reset()