[Zope-CVS] CVS: Products/Ape/lib/apelib/fs - fileops.py:1.1 connection.py:1.3

Shane Hathaway shane@zope.com
Sat, 24 May 2003 17:51:27 -0400


Update of /cvs-repository/Products/Ape/lib/apelib/fs
In directory cvs.zope.org:/tmp/cvs-serv17360

Modified Files:
	connection.py 
Added Files:
	fileops.py 
Log Message:
Abstracted file I/O into a file operations object, with the intent of
later providing alternate implementations such as archive import/export.


=== Added File Products/Ape/lib/apelib/fs/fileops.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Filesystem I/O abstraction.

$Id: fileops.py,v 1.1 2003/05/24 21:51:26 shane Exp $
"""

import os
import shutil


class StandardFileOperations:
    """Standard filesystem interaction implementation.

    Provides the operations needed by FSConnection.
    """

    def __init__(self):
        self.dirname = os.path.dirname
        self.exists = os.path.exists
        self.getmtime = os.path.getmtime
        self.isdir = os.path.isdir
        self.join = os.path.join
        self.listdir =  os.listdir
        self.makedirs = os.makedirs
        self.mkdir = os.mkdir
        self.remove = os.remove
        self.rename = os.rename
        self.rmtree = shutil.rmtree
        self.split = os.path.split
        self.splitext = os.path.splitext
        
    def readfile(self, path, as_text):
        f = open(path, as_text and 'rt' or 'rb')
        try:
            return f.read()
        finally:
            f.close()

    def writefile(self, path, as_text, bytes):
        f = open(path, as_text and 'wt' or 'wb')
        try:
            f.write(bytes)
        finally:
            f.close()

    def canwrite(self, path):
        return os.access(path, os.W_OK)



=== Products/Ape/lib/apelib/fs/connection.py 1.2 => 1.3 ===
--- Products/Ape/lib/apelib/fs/connection.py:1.2	Mon Apr 21 13:26:33 2003
+++ Products/Ape/lib/apelib/fs/connection.py	Sat May 24 17:51:26 2003
@@ -16,9 +16,7 @@
 $Id$
 """
 
-import os
 import re
-from shutil import rmtree
 from types import StringType
 
 from apelib.core.interfaces import ITPCConnection
@@ -26,6 +24,7 @@
 from interfaces import IFSConnection
 from exceptions import FSWriteError
 from cache import ShortLivedCache
+from fileops import StandardFileOperations
 
 
 # Try to decipher this regular expression ;-)
@@ -67,7 +66,8 @@
 
     basepath = ''
 
-    def __init__(self, basepath, metadata_prefix='.', hidden_filenames='_'):
+    def __init__(self, basepath, metadata_prefix='.', hidden_filenames='_',
+                 ops=None):
         self.basepath = basepath
         self.metadata_prefix = metadata_prefix
         self.hidden_re = re.compile(hidden_filenames)
@@ -77,6 +77,9 @@
         self._pending = {}
         self._props_cache = ShortLivedCache()
         self._dir_cache = ShortLivedCache()
+        if ops is None:
+            ops = StandardFileOperations()
+        self.ops = ops
 
 
     def _isLegalFilename(self, fn):
@@ -101,7 +104,7 @@
         obj_names = []
         trans = {}     # { base name -> filename with extension or None }
         try:
-            fns = os.listdir(path)
+            fns = self.ops.listdir(path)
         except OSError:
             if ignore_error:
                 return ([], obj_names, trans)
@@ -131,7 +134,7 @@
         res = (filenames, obj_names, trans)
         self._dir_cache.set(path, res)
         return res
-        
+
 
     def _listDirectoryAsMapping(self, path, ignore_error=0):
         """Returns the translated filenames at path.
@@ -158,12 +161,12 @@
         if self.basepath:
             while subpath.startswith('/') or subpath.startswith('\\'):
                 subpath = subpath[1:]
-            path = os.path.join(self.basepath, subpath)
+            path = self.ops.join(self.basepath, subpath)
         else:
             # unchanged.
             path = subpath
-        if not os.path.exists(path):
-            dir_path, obj_name = os.path.split(path)
+        if not self.ops.exists(path):
+            dir_path, obj_name = self.ops.split(path)
             if '.' not in obj_name:
                 # This object might have an automatic filename extension.
                 filenames, obj_names, trans = self._computeDirectoryContents(
@@ -171,7 +174,7 @@
                 fn = trans.get(obj_name)
                 if fn is not None:
                     # Use the filename with an extension.
-                    path = os.path.join(dir_path, fn)
+                    path = self.ops.join(dir_path, fn)
         return path
 
 
@@ -212,39 +215,30 @@
 
     def readNodeType(self, subpath):
         path = self._expandPath(subpath)
-        if not os.path.exists(path):
+        if not self.ops.exists(path):
             raise NoStateFoundError(subpath)
-        return os.path.isdir(path) and 'd' or 'f'
+        return self.ops.isdir(path) and 'd' or 'f'
 
 
     def readData(self, subpath, allow_missing=0, as_text=0):
         path = self._expandPath(subpath)
-        isdir = os.path.isdir(path)
+        isdir = self.ops.isdir(path)
         # Read either the directory listing or the file contents.
         if isdir:
             # Return a sequence of object names.
             return self._listDirectoryAsMapping(path).values()
-        else:
-            # Return a string.
-            if as_text:
-                mode = 'rt'
-            else:
-                mode = 'rb'
-            try:
-                f = open(path, mode)
-            except IOError:
-                if allow_missing:
-                    return None
-                raise
-            try:
-                return f.read()
-            finally:
-                f.close()
+        # Return a string.
+        try:
+            return self.ops.readfile(path, as_text)
+        except IOError:
+            if allow_missing:
+                return None
+            raise
 
 
     def getExtension(self, subpath):
         path = self._expandPath(subpath)
-        stuff, ext = os.path.splitext(path)
+        stuff, ext = self.ops.splitext(path)
         return ext
 
 
@@ -260,7 +254,7 @@
         maxtime = -1
         for p in (path, props, remainder):
             try:
-                t = os.path.getmtime(p)
+                t = self.ops.getmtime(p)
             except OSError:
                 pass
             else:
@@ -273,11 +267,11 @@
 
     def _getPropertyPaths(self, path):
         """Returns the property and remainder paths for a path."""
-        if os.path.isdir(path):
-            base_fn = os.path.join(path, self.metadata_prefix)
+        if self.ops.isdir(path):
+            base_fn = self.ops.join(path, self.metadata_prefix)
         else:
-            dirname, filename = os.path.split(path)
-            base_fn = os.path.join(dirname, '%s%s.' % (
+            dirname, filename = self.ops.split(path)
+            base_fn = self.ops.join(dirname, '%s%s.' % (
                 self.metadata_prefix, filename))
         return (base_fn + PROPERTIES_EXTENSION, base_fn + REMAINDER_EXTENSION)
 
@@ -292,29 +286,21 @@
 
         res = {}
         try:
-            f = open(rem_fn, 'rb')
+            data = self.ops.readfile(rem_fn, 0)
         except IOError:
             # The remainder file apparently does not exist
             pass
         else:
-            try:
-                data = f.read()
-            finally:
-                f.close()
             res[REMAINDER_SECTION] = data
             # Note that the remainder can be overridden by the properties
             # file.  Perhaps that should be prevented in the future.
 
         try:
-            f = open(props_fn, 'rt')
+            data = self.ops.readfile(props_fn, 1)
         except IOError:
             # The properties file apparently does not exist
             self._props_cache.set(path, res)
             return res
-        try:
-            data = f.read()
-        finally:
-            f.close()
 
         pos = 0
         prev_section_name = None
@@ -343,11 +329,11 @@
         # sections is a mapping.
         path = self._expandPath(subpath)
         t = sections[NODE_TYPE_SECTION]
-        if not os.path.exists(path):
+        if not self.ops.exists(path):
             if t == 'd':
-                os.mkdir(path)
+                self.ops.mkdir(path)
             else:
-                fn = os.path.split(path)[1]
+                fn = self.ops.split(path)[1]
                 if '.' not in fn:
                     # This object has no extension and doesn't yet exist.
                     ext = sections.get(SUGGESTED_EXTENSION_SECTION)
@@ -356,13 +342,13 @@
                         if not ext.startswith('.'):
                             ext = '.' + ext
                         p = path + ext
-                        if not os.path.exists(p):
+                        if not self.ops.exists(p):
                             # No file is in the way.
                             # Use the suggested extension.
                             path = p
         props_fn, rem_fn = self._getPropertyPaths(path)
-        props_f = None
-        rem_f = None
+        props_data = ''
+        rem_data = ''
         items = sections.items()
         items.sort()
         try:
@@ -374,66 +360,45 @@
                     if t == 'd':
                         # Change the list of subobjects.
                         self._removeUnlinkedItems(path, data)
-                        if props_f is None:
-                            props_f = open(props_fn, 'wt')
-                        self._writeToProperties(props_f, OBJECT_NAMES_SECTION,
-                                                '\n'.join(data))
+                        props_data += self._formatSection(
+                            OBJECT_NAMES_SECTION, '\n'.join(data))
                         self._disableConflictingExtensions(subpath, data)
                         self._dir_cache.invalidate(path)
                     else:
                         # Change the file contents.
-                        if as_text:
-                            mode = 'wt'
-                        else:
-                            mode = 'wb'
-                        f = open(path, mode)
-                        try:
-                            f.write(data)
-                        finally:
-                            f.close()
+                        self.ops.writefile(path, as_text, data)
                 elif name == SUGGESTED_EXTENSION_SECTION:
                     # This doesn't need to be written.
                     pass
                 elif name == REMAINDER_SECTION:
                     # Write to the remainder file.
-                    if rem_f is None:
-                        rem_f = open(rem_fn, 'wb')
-                    rem_f.write(value)
+                    rem_data = value
                 else:
                     # Write a metadata section.
-                    if props_f is None:
-                        props_f = open(props_fn, 'wt')
-                    self._writeToProperties(props_f, name, value)
+                    props_data += self._formatSection(name, value)
         finally:
-            self._closeOrDelete(props_f, props_fn)
-            self._closeOrDelete(rem_f, rem_fn)
+            self._writeOrRemove(props_fn, 1, props_data)
+            self._writeOrRemove(rem_fn, 0, rem_data)
             self._props_cache.invalidate(path)
             # The file might be new, so invalidate the directory.
-            self._dir_cache.invalidate(os.path.dirname(path))
-
+            self._dir_cache.invalidate(self.ops.dirname(path))
 
-    def _writeToProperties(self, props_f, name, text):
-        props_f.write('[%s]\n' % name)
-        props_f.write(text.replace('[', '[['))
-        if text.endswith('\n'):
-            props_f.write('\n')
-        else:
-            props_f.write('\n\n')
 
+    def _formatSection(self, name, text):
+        s = '[%s]\n%s\n' % (name, text.replace('[', '[['))
+        if not text.endswith('\n'):
+            s += '\n'
+        return s
 
-    def _closeOrDelete(self, f, fn):
-        """Finish writing or remove a file.
 
-        If f was opened, close it.  If f was not opened, no file is needed,
-        so remove it.
+    def _writeOrRemove(self, fn, as_text, data):
+        """If data is provided, write it.  Otherwise remove the file.
         """
-        if f is not None:
-            f.close()
+        if data:
+            self.ops.writefile(fn, as_text, data)
         else:
-            try:
-                os.remove(fn)
-            except OSError:
-                pass
+            if self.ops.exists(fn):
+                self.ops.remove(fn)
 
 
     def _removeUnlinkedItems(self, path, names):
@@ -443,16 +408,16 @@
             linked[name] = 1
         for fn, obj_name in self._listDirectoryAsMapping(path).items():
             if not linked.get(obj_name):
-                item_fn = os.path.join(path, fn)
-                if os.path.isdir(item_fn):
-                    rmtree(item_fn)
+                item_fn = self.ops.join(path, fn)
+                if self.ops.isdir(item_fn):
+                    self.ops.rmtree(item_fn)
                 else:
-                    os.remove(item_fn)
+                    self.ops.remove(item_fn)
                     props_fn, rem_fn = self._getPropertyPaths(item_fn)
-                    if os.path.exists(props_fn):
-                        os.remove(props_fn)
-                    if os.path.exists(rem_fn):
-                        os.remove(rem_fn)
+                    if self.ops.exists(props_fn):
+                        self.ops.remove(props_fn)
+                    if self.ops.exists(rem_fn):
+                        self.ops.remove(rem_fn)
 
 
     def _disableConflictingExtensions(self, subpath, obj_names):
@@ -490,8 +455,8 @@
         non_containers = {}
         for subpath, sections in items:
             path = self._expandPath(subpath)
-            exists = os.path.exists(path)
-            if exists and not os.access(path, os.W_OK):
+            exists = self.ops.exists(path)
+            if exists and not self.ops.canwrite(path):
                 raise FSWriteError(
                     "Can't get write access to %s" % subpath)
             # type must be provided and must always be either 'd' or 'f'.
@@ -500,13 +465,13 @@
                 raise FSWriteError(
                     'Data or node type not specified for %s' % subpath)
             t = sections[NODE_TYPE_SECTION]
-            dir = os.path.dirname(subpath)
+            dir = self.ops.dirname(subpath)
             if non_containers.get(dir):
                 raise FSWriteError(
                     "Not a directory: %s" % dir)
             data, as_text = sections[DATA_SECTION]
             if t == 'f':
-                if exists and os.path.isdir(path):
+                if exists and self.ops.isdir(path):
                     raise FSWriteError(
                         "Can't write file data to directory at %s"
                         % subpath)
@@ -516,7 +481,7 @@
                         'Data for a file must be a string at %s'
                         % subpath)
             elif t == 'd':
-                if exists and not os.path.isdir(path):
+                if exists and not self.ops.isdir(path):
                     raise FSWriteError(
                         "Can't write directory contents to file at %s"
                         % subpath)
@@ -560,8 +525,8 @@
         return self.basepath
 
     def connect(self):
-        if not os.path.exists(self.basepath):
-            os.makedirs(self.basepath)
+        if not self.ops.exists(self.basepath):
+            self.ops.makedirs(self.basepath)
 
     def begin(self):
         self._props_cache.clear()