6 from allmydata.scripts.common import get_alias, escape_path, DEFAULT_ALIAS
7 from allmydata.scripts.common_http import do_http
8 from allmydata import uri
9 from allmydata.util import time_format
11 def raiseHTTPError(msg, resp):
12 msg = msg + ": %s %s %s" % (resp.status, resp.reason, resp.read())
13 raise RuntimeError(msg)
15 def readonly(writedircap):
16 return uri.from_string_dirnode(writedircap).get_readonly().to_string()
18 def parse_old_timestamp(s, options):
20 if not s.endswith("Z"):
22 # the "local" in this "localseconds" is superfluous and
23 # misleading. This returns seconds-since-epoch for an
24 # ISO-8601-ish-formatted UTC time string. This might raise
25 # ValueError if the string is not in the right format.
26 when = time_format.iso_utc_time_to_localseconds(s[:-1])
31 # "2008-11-16 10.34 PM" (localtime)
32 if s[-3:] in (" AM", " PM"):
33 # this might raise ValueError
34 when = time.strptime(s[:-3], "%Y-%m-%d %H.%M")
40 print >>options.stderr, "unable to parse old timestamp '%s', ignoring" % s
42 def readdir(dircap, options):
43 # returns a dict of (childname: (type, readcap, metadata)), or None if the
44 # dircap didn't point to a directory
45 url = options['node-url'] + "uri/%s?t=json" % urllib.quote(dircap)
46 resp = do_http("GET", url)
47 if resp.status != 200:
48 raiseHTTPError("Error during directory GET", resp)
49 jd = simplejson.load(resp)
51 if ntype != "dirnode":
54 for (childname, (childtype, childdata)) in ndata["children"].items():
55 contents[childname] = (childtype,
56 str(childdata["ro_uri"]),
57 childdata["metadata"])
60 def get_local_metadata(path):
63 # posix stat(2) metadata, depends on the platform
64 os.stat_float_times(True)
66 metadata["ctime"] = s.st_ctime
67 metadata["mtime"] = s.st_mtime
69 misc_fields = ("st_mode", "st_ino", "st_dev", "st_uid", "st_gid")
70 macos_misc_fields = ("st_rsize", "st_creator", "st_type")
71 for field in misc_fields + macos_misc_fields:
73 metadata[field] = getattr(s, field)
75 # TODO: extended attributes, like on OS-X's HFS+
78 def mkdir(contents, options):
79 url = options['node-url'] + "uri?t=mkdir"
80 resp = do_http("POST", url)
81 if resp.status < 200 or resp.status >= 300:
82 raiseHTTPError("error during mkdir", resp)
83 dircap = str(resp.read().strip())
84 url = options['node-url'] + "uri/%s?t=set_children" % urllib.quote(dircap)
85 body = dict([ (childname, (contents[childname][0],
86 {"ro_uri": contents[childname][1],
87 "metadata": contents[childname][2],
89 for childname in contents
91 resp = do_http("POST", url, simplejson.dumps(body))
92 if resp.status != 200:
93 raiseHTTPError("error during set_children", resp)
96 def put_child(dirurl, childname, childcap):
97 assert dirurl[-1] == "/"
98 url = dirurl + urllib.quote(childname) + "?t=uri"
99 resp = do_http("PUT", url, childcap)
100 if resp.status not in (200, 201):
101 raiseHTTPError("error during put_child", resp)
103 def directory_is_changed(a, b):
104 # each is a mapping from childname to (type, cap, metadata)
105 significant_metadata = ("ctime", "mtime")
106 # other metadata keys are preserved, but changes to them won't trigger a
109 if set(a.keys()) != set(b.keys()):
112 a_type, a_cap, a_metadata = a[childname]
113 b_type, b_cap, b_metadata = b[childname]
118 for k in significant_metadata:
119 if a_metadata.get(k) != b_metadata.get(k):
124 nodeurl = options['node-url']
125 from_dir = options.from_dir
126 to_dir = options.to_dir
131 stdin = options.stdin
132 stdout = options.stdout
133 stderr = options.stderr
135 rootcap, path = get_alias(options.aliases, options.to_dir, DEFAULT_ALIAS)
136 to_url = nodeurl + "uri/%s/" % urllib.quote(rootcap)
138 to_url += escape_path(path)
139 if not to_url.endswith("/"):
142 archives_url = to_url + "Archives/"
143 latest_url = to_url + "Latest"
145 # first step: make sure the target directory exists, as well as the
146 # Archives/ subdirectory.
147 resp = do_http("GET", archives_url + "?t=json")
148 if resp.status == 404:
149 resp = do_http("POST", archives_url + "?t=mkdir")
150 if resp.status != 200:
151 print >>stderr, "Unable to create target directory: %s %s %s" % \
152 (resp.status, resp.reason, resp.read())
156 jdata = simplejson.load(resp)
157 (otype, attrs) = jdata
158 archives_dir = attrs["children"]
160 # second step: locate the most recent backup in TODIR/Archives/*
161 latest_backup_time = 0
162 latest_backup_name = None
163 latest_backup_dircap = None
165 # we have various time formats. The allmydata.com windows backup tool
166 # appears to create things like "2008-11-16 10.34 PM". This script
167 # creates things like "2009-11-16--17.34Z".
168 for archive_name in archives_dir.keys():
169 if archives_dir[archive_name][0] != "dirnode":
171 when = parse_old_timestamp(archive_name, options)
173 if when > latest_backup_time:
174 latest_backup_time = when
175 latest_backup_name = archive_name
176 latest_backup_dircap = str(archives_dir[archive_name][1]["ro_uri"])
178 # third step: process the tree
179 new_backup_dircap = Node().process(options.from_dir,
180 latest_backup_dircap,
182 print >>stdout, "new backup done"
184 # fourth: attach the new backup to the list
185 new_readonly_backup_dircap = readonly(new_backup_dircap)
186 now = time_format.iso_utc(int(time.time()), sep="_") + "Z"
188 put_child(archives_url, now, new_readonly_backup_dircap)
189 put_child(to_url, "Latest", new_readonly_backup_dircap)
191 print >>stdout, "backup done"
197 def verboseprint(self, msg):
198 if self.options["verbose"]:
199 print >>self.options.stdout, msg
201 def process(self, localpath, olddircap, options):
203 self.options = options
205 self.verboseprint("processing %s, olddircap %s" % (localpath, olddircap))
208 olddircontents = readdir(olddircap, options)
210 newdircontents = {} # childname -> (type, rocap, metadata)
211 for child in os.listdir(localpath):
212 childpath = os.path.join(localpath, child)
213 if os.path.isdir(childpath):
214 metadata = get_local_metadata(childpath)
216 if olddircontents is not None and child in olddircontents:
217 oldchildcap = olddircontents[child][1]
218 newchilddircap = self.recurse(childpath, oldchildcap)
219 newdircontents[child] = ("dirnode", newchilddircap, metadata)
220 elif os.path.isfile(childpath):
221 newfilecap, metadata = self.upload(childpath)
222 newdircontents[child] = ("filenode", newfilecap, metadata)
224 raise RuntimeError("how do I back this up?")
227 and olddircontents is not None
228 and not directory_is_changed(newdircontents, olddircontents)
230 self.verboseprint(" %s not changed, re-using old directory" % localpath)
231 # yay! they're identical!
234 self.verboseprint(" %s changed, making new directory" % localpath)
235 # something changed, or there was no previous directory, so we
236 # must make a new directory
237 newdircap = mkdir(newdircontents, options)
238 return readonly(newdircap)
240 def recurse(self, localpath, olddircap):
242 return n.process(localpath, olddircap, self.options)
244 def upload(self, childpath):
245 self.verboseprint("uploading %s.." % childpath)
246 # we can use the backupdb here
247 #s = os.stat(childpath)
249 # if we go with the old file, we're obligated to use the old
250 # metadata, to make sure it matches the metadata for this child in
251 # the old parent directory
252 # return oldcap, old_metadata
254 metadata = get_local_metadata(childpath)
255 infileobj = open(os.path.expanduser(childpath), "rb")
256 url = self.options['node-url'] + "uri"
257 resp = do_http("PUT", url, infileobj)
258 if resp.status not in (200, 201):
259 raiseHTTPError("Error during file PUT", resp)
260 filecap = resp.read().strip()
261 self.verboseprint(" %s -> %s" % (childpath, filecap))
262 self.verboseprint(" metadata: %s" % (metadata,))
263 return filecap, metadata