root/trunk/ftp_sync.py

Revision 763, 6.5 kB (checked in by schwa, 7 months ago)
Added svn:mime-type text/x-python to the Python files which didn't
have the MIME type set.
  • Property svn:mime-type set to text/x-python
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 # Copyright (C) 2007, Stefan Schwarzer
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # - Redistributions of source code must retain the above copyright
9 #   notice, this list of conditions and the following disclaimer.
10 #
11 # - Redistributions in binary form must reproduce the above copyright
12 #   notice, this list of conditions and the following disclaimer in the
13 #   documentation and/or other materials provided with the distribution.
14 #
15 # - Neither the name of the above author nor the names of the
16 #   contributors to the software may be used to endorse or promote
17 #   products derived from this software without specific prior written
18 #   permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
24 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 # $Id$
33
34 """
35 Tools for syncing combinations of local and remote directories.
36
37 *** WARNING: This is an unfinished in-development version!
38 """
39
40 # Sync combinations:
41 # - remote -> local (download)
42 # - local -> remote (upload)
43 # - remote -> remote
44 # - local -> local (perhaps implicitly possible due to design, but not targeted)
45
46 import os
47 import shutil
48
49 from ftputil import FTPHost
50 import ftp_error
51
52 __all__ = ['FTPHost', 'LocalHost', 'Syncer']
53
54
55 # used for copying file objects; value is 64 KB
56 CHUNK_SIZE = 64*1024
57
58
59 class LocalHost(object):
60     def open(self, path, mode):
61         """
62         Return a Python file object for file name `path`, opened in
63         mode `mode`.
64         """
65         # this is the built-in `open` function, not `os.open`!
66         return open(path, mode)
67
68     def time_shift(self):
69         """
70         Return the time shift value (see methods `set_time_shift`
71         and `time_shift` in class `FTPHost` for a definition). By
72         definition, the value is zero for local file systems.
73         """
74         return 0.0
75
76     def __getattr__(self, attr):
77         return getattr(os, attr)
78
79
80 class Syncer(object):
81     def __init__(self, source, target):
82         """
83         Init the `FTPSyncer` instance.
84
85         Each of `source` and `target` is either an `FTPHost` or a
86         `LocalHost` object. The source and target directories, resp.
87         have to be set with the `chdir` command before passing them
88         in. The semantics is so that the items under the source
89         directory will show up under the target directory after the
90         synchronization (unless there's an error).
91         """
92         self._source = source
93         self._target = target
94
95     def _mkdir(self, target_dir):
96         """
97         Try to create the target directory `target_dir`. If it already
98         exists, don't do anything. If the directory is present but
99         it's actually a file, raise a `SyncError`.
100         """
101         #TODO handle setting of target mtime according to source mtime
102         #  (beware of rootdir anomalies; try to handle them as well)
103         #print "Making", target_dir
104         if self._target.path.isfile(target_dir):
105             raise ftp_error.SyncError("target dir '%s' is actually a file" %
106                                       target_dir)
107         if not self._target.path.isdir(target_dir):
108             self._target.mkdir(target_dir)
109
110     def _sync_file(self, source_file, target_file):
111         #XXX this duplicates code from `FTPHost._copyfileobj`; maybe
112         #  implement the upload and download methods in terms of
113         #  `_sync_file`, or maybe not?
114         #TODO handle `IOError`s
115         #TODO handle conditional copy
116         #TODO handle setting of target mtime according to source mtime
117         #  (beware of rootdir anomalies; try to handle them as well)
118         print "Syncing", source_file, "->", target_file
119         source = self._source.open(source_file, "rb")
120         try:
121             target = self._target.open(target_file, "wb")
122             try:
123                 shutil.copyfileobj(source, target, length=CHUNK_SIZE)
124             finally:
125                 target.close()
126         finally:
127             source.close()
128
129     def _sync_tree(self, source_dir, target_dir):
130         """
131         Synchronize the source and the target directory tree by
132         updating the target to match the source as far as possible.
133
134         Current limitations:
135         - _don't_ delete items which are on the target path but not on the
136           source path
137         - files are always copied, the modification timestamps are not
138           compared
139         - all files are copied in binary mode, never in ASCII/text mode
140         - incomplete error handling
141         """
142         self._mkdir(target_dir)
143         for dirpath, dirnames, filenames in self._source.walk(source_dir):
144             for dirname in dirnames:
145                 inner_source_dir = self._source.path.join(dirpath, dirname)
146                 inner_target_dir = inner_source_dir.replace(source_dir,
147                                                             target_dir, 1)
148                 self._mkdir(inner_target_dir)
149             for filename in filenames:
150                 source_file = self._source.path.join(dirpath, filename)
151                 target_file = source_file.replace(source_dir, target_dir, 1)
152                 self._sync_file(source_file, target_file)
153
154     def sync(self, source_path, target_path):
155         """
156         Synchronize `source_path` and `target_path` (both are strings,
157         each denoting a directory or file path), i. e. update the
158         target path so that it's a copy of the source path.
159
160         This method handles both directory trees and single files.
161         """
162         #TODO handle making of missing intermediate directories
163         source_path = self._source.path.abspath(source_path)
164         target_path = self._target.path.abspath(target_path)
165         if self._source.path.isfile(source_path):
166             self._sync_file(source_path, target_path)
167         else:
168             self._sync_tree(source_path, target_path)
169
Note: See TracBrowser for help on using the browser.