source file: /home/buildslave/tahoe/edgy/build/src/allmydata/scripts/tahoe_cp.py
file stats: 525 lines, 469 executed: 89.3% covered
1.
2. import os.path
3. import urllib
4. import simplejson
5. from allmydata.scripts.common import get_alias, escape_path, DefaultAliasMarker
6. from allmydata.scripts.common_http import do_http
7. from allmydata import uri
8.
9. def ascii_or_none(s):
10. if s is None:
11. return s
12. return str(s)
13.
14. class WriteError(Exception):
15. pass
16. class ReadError(Exception):
17. pass
18. class MissingSourceError(Exception):
19. pass
20.
21. def GET_to_file(url):
22. resp = do_http("GET", url)
23. if resp.status == 200:
24. return resp
25. raise ReadError("Error during GET: %s %s %s" % (resp.status,
26. resp.reason,
27. resp.read()))
28. def GET_to_string(url):
29. f = GET_to_file(url)
30. return f.read()
31.
32. def PUT(url, data):
33. resp = do_http("PUT", url, data)
34. if resp.status in (200, 201):
35. return resp.read()
36. raise WriteError("Error during PUT: %s %s %s" % (resp.status, resp.reason,
37. resp.read()))
38.
39. def POST(url, data):
40. resp = do_http("POST", url, data)
41. if resp.status in (200, 201):
42. return resp.read()
43. raise WriteError("Error during POST: %s %s %s" % (resp.status, resp.reason,
44. resp.read()))
45.
46. def mkdir(targeturl):
47. url = targeturl + "?t=mkdir"
48. resp = do_http("POST", url)
49. if resp.status in (200, 201):
50. return resp.read().strip()
51. raise WriteError("Error during mkdir: %s %s %s" % (resp.status, resp.reason,
52. resp.read()))
53.
54. def make_tahoe_subdirectory(nodeurl, parent_writecap, name):
55. url = nodeurl + "/".join(["uri",
56. urllib.quote(parent_writecap),
57. urllib.quote(name),
58. ]) + "?t=mkdir"
59. resp = do_http("POST", url)
60. if resp.status in (200, 201):
61. return resp.read().strip()
62. raise WriteError("Error during mkdir: %s %s %s" % (resp.status, resp.reason,
63. resp.read()))
64.
65.
66. class LocalFileSource:
67. def __init__(self, pathname):
68. self.pathname = pathname
69.
70. def need_to_copy_bytes(self):
71. return True
72.
73. def open(self):
74. return open(self.pathname, "rb")
75.
76. class LocalFileTarget:
77. def __init__(self, pathname):
78. self.pathname = pathname
79. def put_file(self, inf):
80. outf = open(self.pathname, "wb")
81. while True:
82. data = inf.read(32768)
83. if not data:
84. break
85. outf.write(data)
86. outf.close()
87.
88. class LocalMissingTarget:
89. def __init__(self, pathname):
90. self.pathname = pathname
91.
92. def put_file(self, inf):
93. outf = open(self.pathname, "wb")
94. while True:
95. data = inf.read(32768)
96. if not data:
97. break
98. outf.write(data)
99. outf.close()
100.
101. class LocalDirectorySource:
102. def __init__(self, progressfunc, pathname):
103. self.progressfunc = progressfunc
104. self.pathname = pathname
105. self.children = None
106.
107. def populate(self, recurse):
108. if self.children is not None:
109. return
110. self.children = {}
111. children = os.listdir(self.pathname)
112. for i,n in enumerate(children):
113. self.progressfunc("examining %d of %d" % (i, len(children)))
114. pn = os.path.join(self.pathname, n)
115. if os.path.isdir(pn):
116. child = LocalDirectorySource(self.progressfunc, pn)
117. self.children[n] = child
118. if recurse:
119. child.populate(True)
120. else:
121. assert os.path.isfile(pn)
122. self.children[n] = LocalFileSource(pn)
123.
124. class LocalDirectoryTarget:
125. def __init__(self, progressfunc, pathname):
126. self.progressfunc = progressfunc
127. self.pathname = pathname
128. self.children = None
129.
130. def populate(self, recurse):
131. if self.children is not None:
132. return
133. self.children = {}
134. children = os.listdir(self.pathname)
135. for i,n in enumerate(children):
136. self.progressfunc("examining %d of %d" % (i, len(children)))
137. pn = os.path.join(self.pathname, n)
138. if os.path.isdir(pn):
139. child = LocalDirectoryTarget(self.progressfunc, pn)
140. self.children[n] = child
141. if recurse:
142. child.populate(True)
143. else:
144. assert os.path.isfile(pn)
145. self.children[n] = LocalFileTarget(pn)
146.
147. def get_child_target(self, name):
148. if self.children is None:
149. self.populate(False)
150. if name in self.children:
151. return self.children[name]
152. pathname = os.path.join(self.pathname, name)
153. os.makedirs(pathname)
154. return LocalDirectoryTarget(self.progressfunc, pathname)
155.
156. def put_file(self, name, inf):
157. pathname = os.path.join(self.pathname, name)
158. outf = open(pathname, "wb")
159. while True:
160. data = inf.read(32768)
161. if not data:
162. break
163. outf.write(data)
164. outf.close()
165.
166. def set_children(self):
167. pass
168.
169. class TahoeFileSource:
170. def __init__(self, nodeurl, mutable, writecap, readcap):
171. self.nodeurl = nodeurl
172. self.mutable = mutable
173. self.writecap = writecap
174. self.readcap = readcap
175.
176. def need_to_copy_bytes(self):
177. if self.mutable:
178. return True
179. return False
180.
181. def open(self):
182. url = self.nodeurl + "uri/" + urllib.quote(self.readcap)
183. return GET_to_file(url)
184.
185. def bestcap(self):
186. return self.writecap or self.readcap
187.
188. class TahoeFileTarget:
189. def __init__(self, nodeurl, mutable, writecap, readcap, url):
190. self.nodeurl = nodeurl
191. self.mutable = mutable
192. self.writecap = writecap
193. self.readcap = readcap
194. self.url = url
195.
196. def put_file(self, inf):
197. # We want to replace this object in-place.
198. assert self.url
199. # our do_http() call currently requires a string or a filehandle with
200. # a real .seek
201. if not hasattr(inf, "seek"):
202. inf = inf.read()
203. PUT(self.url, inf)
204. # TODO: this always creates immutable files. We might want an option
205. # to always create mutable files, or to copy mutable files into new
206. # mutable files.
207.
208. class TahoeDirectorySource:
209. def __init__(self, nodeurl, cache, progressfunc):
210. self.nodeurl = nodeurl
211. self.cache = cache
212. self.progressfunc = progressfunc
213.
214. def init_from_grid(self, writecap, readcap):
215. self.writecap = writecap
216. self.readcap = readcap
217. bestcap = writecap or readcap
218. url = self.nodeurl + "uri/%s" % urllib.quote(bestcap)
219. resp = do_http("GET", url + "?t=json")
220. assert resp.status == 200
221. parsed = simplejson.loads(resp.read())
222. nodetype, d = parsed
223. assert nodetype == "dirnode"
224. self.mutable = d.get("mutable", False) # older nodes don't provide it
225. self.children_d = d["children"]
226. self.children = None
227.
228. def init_from_parsed(self, parsed):
229. nodetype, d = parsed
230. self.writecap = ascii_or_none(d.get("rw_uri"))
231. self.readcap = ascii_or_none(d.get("ro_uri"))
232. self.mutable = d.get("mutable", False) # older nodes don't provide it
233. self.children_d = d["children"]
234. self.children = None
235.
236. def populate(self, recurse):
237. if self.children is not None:
238. return
239. self.children = {}
240. for i,(name, data) in enumerate(self.children_d.items()):
241. self.progressfunc("examining %d of %d" % (i, len(self.children_d)))
242. if data[0] == "filenode":
243. mutable = data[1].get("mutable", False)
244. writecap = ascii_or_none(data[1].get("rw_uri"))
245. readcap = ascii_or_none(data[1].get("ro_uri"))
246. self.children[name] = TahoeFileSource(self.nodeurl, mutable,
247. writecap, readcap)
248. else:
249. assert data[0] == "dirnode"
250. writecap = ascii_or_none(data[1].get("rw_uri"))
251. readcap = ascii_or_none(data[1].get("ro_uri"))
252. if writecap and writecap in self.cache:
253. child = self.cache[writecap]
254. elif readcap and readcap in self.cache:
255. child = self.cache[readcap]
256. else:
257. child = TahoeDirectorySource(self.nodeurl, self.cache,
258. self.progressfunc)
259. child.init_from_grid(writecap, readcap)
260. if writecap:
261. self.cache[writecap] = child
262. if readcap:
263. self.cache[readcap] = child
264. if recurse:
265. child.populate(True)
266. self.children[name] = child
267.
268. class TahoeMissingTarget:
269. def __init__(self, url):
270. self.url = url
271.
272. def put_file(self, inf):
273. # We want to replace this object in-place.
274. if not hasattr(inf, "seek"):
275. inf = inf.read()
276. PUT(self.url, inf)
277. # TODO: this always creates immutable files. We might want an option
278. # to always create mutable files, or to copy mutable files into new
279. # mutable files.
280.
281. def put_uri(self, filecap):
282. # I'm not sure this will always work
283. return PUT(self.url + "?t=uri", filecap)
284.
285. class TahoeDirectoryTarget:
286. def __init__(self, nodeurl, cache, progressfunc):
287. self.nodeurl = nodeurl
288. self.cache = cache
289. self.progressfunc = progressfunc
290. self.new_children = {}
291.
292. def init_from_parsed(self, parsed):
293. nodetype, d = parsed
294. self.writecap = ascii_or_none(d.get("rw_uri"))
295. self.readcap = ascii_or_none(d.get("ro_uri"))
296. self.mutable = d.get("mutable", False) # older nodes don't provide it
297. self.children_d = d["children"]
298. self.children = None
299.
300. def init_from_grid(self, writecap, readcap):
301. self.writecap = writecap
302. self.readcap = readcap
303. bestcap = writecap or readcap
304. url = self.nodeurl + "uri/%s" % urllib.quote(bestcap)
305. resp = do_http("GET", url + "?t=json")
306. assert resp.status == 200
307. parsed = simplejson.loads(resp.read())
308. nodetype, d = parsed
309. assert nodetype == "dirnode"
310. self.mutable = d.get("mutable", False) # older nodes don't provide it
311. self.children_d = d["children"]
312. self.children = None
313.
314. def just_created(self, writecap):
315. self.writecap = writecap
316. self.readcap = uri.from_string(writecap).get_readonly().to_string()
317. self.mutable = True
318. self.children_d = {}
319. self.children = {}
320.
321. def populate(self, recurse):
322. if self.children is not None:
323. return
324. self.children = {}
325. for i,(name, data) in enumerate(self.children_d.items()):
326. self.progressfunc("examining %d of %d" % (i, len(self.children_d)))
327. if data[0] == "filenode":
328. mutable = data[1].get("mutable", False)
329. writecap = ascii_or_none(data[1].get("rw_uri"))
330. readcap = ascii_or_none(data[1].get("ro_uri"))
331. url = None
332. if self.writecap:
333. url = self.nodeurl + "/".join(["uri",
334. urllib.quote(self.writecap),
335. urllib.quote(name)])
336. self.children[name] = TahoeFileTarget(self.nodeurl, mutable,
337. writecap, readcap, url)
338. else:
339. assert data[0] == "dirnode"
340. writecap = ascii_or_none(data[1].get("rw_uri"))
341. readcap = ascii_or_none(data[1].get("ro_uri"))
342. if writecap and writecap in self.cache:
343. child = self.cache[writecap]
344. elif readcap and readcap in self.cache:
345. child = self.cache[readcap]
346. else:
347. child = TahoeDirectoryTarget(self.nodeurl, self.cache,
348. self.progressfunc)
349. child.init_from_grid(writecap, readcap)
350. if writecap:
351. self.cache[writecap] = child
352. if readcap:
353. self.cache[readcap] = child
354. if recurse:
355. child.populate(True)
356. self.children[name] = child
357.
358. def get_child_target(self, name):
359. # return a new target for a named subdirectory of this dir
360. if self.children is None:
361. self.populate(False)
362. if name in self.children:
363. return self.children[name]
364. writecap = make_tahoe_subdirectory(self.nodeurl, self.writecap, name)
365. child = TahoeDirectoryTarget(self.nodeurl, self.cache,
366. self.progressfunc)
367. child.just_created(writecap)
368. self.children[name] = child
369. return child
370.
371. def put_file(self, name, inf):
372. url = self.nodeurl + "uri"
373. if not hasattr(inf, "seek"):
374. inf = inf.read()
375. filecap = PUT(url, inf)
376. # TODO: this always creates immutable files. We might want an option
377. # to always create mutable files, or to copy mutable files into new
378. # mutable files.
379. self.new_children[name] = filecap
380.
381. def put_uri(self, name, filecap):
382. self.new_children[name] = filecap
383.
384. def set_children(self):
385. if not self.new_children:
386. return
387. url = (self.nodeurl + "uri/" + urllib.quote(self.writecap)
388. + "?t=set_children")
389. set_data = {}
390. for (name, filecap) in self.new_children.items():
391. # it just so happens that ?t=set_children will accept both file
392. # read-caps and write-caps as ['rw_uri'], and will handle eithe
393. # correctly. So don't bother trying to figure out whether the one
394. # we have is read-only or read-write.
395. set_data[name] = ["filenode", {"rw_uri": filecap}]
396. body = simplejson.dumps(set_data)
397. POST(url, body)
398.
399. class Copier:
400.
401. def do_copy(self, options, progressfunc=None):
402. if options['quiet']:
403. verbosity = 0
404. else:
405. verbosity = 2
406.
407. nodeurl = options['node-url']
408. if nodeurl[-1] != "/":
409. nodeurl += "/"
410. self.nodeurl = nodeurl
411. self.progressfunc = progressfunc
412. self.options = options
413. self.aliases = options.aliases
414. self.verbosity = verbosity
415. self.stdout = options.stdout
416. self.stderr = options.stderr
417. if options["verbose"] and not self.progressfunc:
418. def progress(message):
419. print >>self.stderr, message
420. self.progressfunc = progress
421. self.cache = {}
422. source_specs = options.sources
423. destination_spec = options.destination
424. recursive = self.options["recursive"]
425.
426. target = self.get_target_info(destination_spec)
427.
428. try:
429. sources = [] # list of (name, source object)
430. for ss in source_specs:
431. name, source = self.get_source_info(ss)
432. sources.append( (name, source) )
433. except MissingSourceError, e:
434. self.to_stderr("No such file or directory %s" % e.args[0])
435. return 1
436.
437. have_source_dirs = bool([s for (name,s) in sources
438. if isinstance(s, (LocalDirectorySource,
439. TahoeDirectorySource))])
440.
441. if have_source_dirs and not recursive:
442. self.to_stderr("cannot copy directories without --recursive")
443. return 1
444.
445. if isinstance(target, (LocalFileTarget, TahoeFileTarget)):
446. # cp STUFF foo.txt, where foo.txt already exists. This limits the
447. # possibilities considerably.
448. if len(sources) > 1:
449. self.to_stderr("target '%s' is not a directory" % destination_spec)
450. return 1
451. if have_source_dirs:
452. self.to_stderr("cannot copy directory into a file")
453. return 1
454. name, source = sources[0]
455. return self.copy_file(source, target)
456.
457. if isinstance(target, (LocalMissingTarget, TahoeMissingTarget)):
458. if recursive:
459. return self.copy_to_directory(sources, target)
460. if len(sources) > 1:
461. # if we have -r, we'll auto-create the target directory. Without
462. # it, we'll only create a file.
463. self.to_stderr("cannot copy multiple files into a file without -r")
464. return 1
465. # cp file1 newfile
466. name, source = sources[0]
467. return self.copy_file(source, target)
468.
469. if isinstance(target, (LocalDirectoryTarget, TahoeDirectoryTarget)):
470. return self.copy_to_directory(sources, target)
471.
472. self.to_stderr("unknown target")
473. return 1
474.
475. def to_stderr(self, text):
476. print >>self.stderr, text
477.
478. def get_target_info(self, destination_spec):
479. rootcap, path = get_alias(self.aliases, destination_spec, None)
480. if rootcap == DefaultAliasMarker:
481. # no alias, so this is a local file
482. pathname = os.path.abspath(os.path.expanduser(path))
483. if not os.path.exists(pathname):
484. t = LocalMissingTarget(pathname)
485. elif os.path.isdir(pathname):
486. t = LocalDirectoryTarget(self.progress, pathname)
487. else:
488. assert os.path.isfile(pathname), pathname
489. t = LocalFileTarget(pathname) # non-empty
490. else:
491. # this is a tahoe object
492. url = self.nodeurl + "uri/%s" % urllib.quote(rootcap)
493. if path:
494. url += "/" + escape_path(path)
495. last_slash = path.rfind("/")
496.
497. resp = do_http("GET", url + "?t=json")
498. if resp.status == 404:
499. # doesn't exist yet
500. t = TahoeMissingTarget(url)
501. else:
502. parsed = simplejson.loads(resp.read())
503. nodetype, d = parsed
504. if nodetype == "dirnode":
505. t = TahoeDirectoryTarget(self.nodeurl, self.cache,
506. self.progress)
507. t.init_from_parsed(parsed)
508. else:
509. writecap = ascii_or_none(d.get("rw_uri"))
510. readcap = ascii_or_none(d.get("ro_uri"))
511. mutable = d.get("mutable", False)
512. t = TahoeFileTarget(self.nodeurl, mutable,
513. writecap, readcap, url)
514. return t
515.
516. def get_source_info(self, source_spec):
517. rootcap, path = get_alias(self.aliases, source_spec, None)
518. if rootcap == DefaultAliasMarker:
519. # no alias, so this is a local file
520. pathname = os.path.abspath(os.path.expanduser(path))
521. name = os.path.basename(pathname)
522. if not os.path.exists(pathname):
523. raise MissingSourceError(source_spec)
524. if os.path.isdir(pathname):
525. t = LocalDirectorySource(self.progress, pathname)
526. else:
527. assert os.path.isfile(pathname)
528. t = LocalFileSource(pathname) # non-empty
529. else:
530. # this is a tahoe object
531. url = self.nodeurl + "uri/%s" % urllib.quote(rootcap)
532. name = None
533. if path:
534. url += "/" + escape_path(path)
535. last_slash = path.rfind("/")
536. name = path
537. if last_slash:
538. name = path[last_slash+1:]
539.
540. resp = do_http("GET", url + "?t=json")
541. if resp.status == 404:
542. raise MissingSourceError(source_spec)
543. parsed = simplejson.loads(resp.read())
544. nodetype, d = parsed
545. if nodetype == "dirnode":
546. t = TahoeDirectorySource(self.nodeurl, self.cache,
547. self.progress)
548. t.init_from_parsed(parsed)
549. else:
550. writecap = ascii_or_none(d.get("rw_uri"))
551. readcap = ascii_or_none(d.get("ro_uri"))
552. mutable = d.get("mutable", False) # older nodes don't provide it
553. t = TahoeFileSource(self.nodeurl, mutable, writecap, readcap)
554. return name, t
555.
556.
557. def dump_graph(self, s, indent=" "):
558. for name, child in s.children.items():
559. print indent + name + ":" + str(child)
560. if isinstance(child, (LocalDirectorySource, TahoeDirectorySource)):
561. self.dump_graph(child, indent+" ")
562.
563. def copy_to_directory(self, source_infos, target):
564. # step one: build a recursive graph of the source tree. This returns
565. # a dictionary, with child names as keys, and values that are either
566. # Directory or File instances (local or tahoe).
567. source_dirs = self.build_graphs(source_infos)
568. source_files = [source for source in source_infos
569. if isinstance(source[1], (LocalFileSource,
570. TahoeFileSource))]
571.
572. #print "graphs"
573. #for s in source_dirs:
574. # self.dump_graph(s)
575.
576. # step two: create the top-level target directory object
577. if isinstance(target, LocalMissingTarget):
578. os.makedirs(target.pathname)
579. target = LocalDirectoryTarget(self.progress, target.pathname)
580. elif isinstance(target, TahoeMissingTarget):
581. writecap = mkdir(target.url)
582. target = TahoeDirectoryTarget(self.nodeurl, self.cache,
583. self.progress)
584. target.just_created(writecap)
585. assert isinstance(target, (LocalDirectoryTarget, TahoeDirectoryTarget))
586. target.populate(False)
587.
588. # step three: find a target for each source node, creating
589. # directories as necessary. 'targetmap' is a dictionary that uses
590. # target Directory instances as keys, and has values of
591. # (name->sourceobject) dicts for all the files that need to wind up
592. # there.
593.
594. # sources are all LocalFile/LocalDirectory/TahoeFile/TahoeDirectory
595. # target is LocalDirectory/TahoeDirectory
596.
597. self.progress("attaching sources to targets, "
598. "%d files / %d dirs in root" %
599. (len(source_files), len(source_dirs)))
600.
601. self.targetmap = {}
602. self.files_to_copy = 0
603.
604. for (name,s) in source_files:
605. self.attach_to_target(s, name, target)
606. self.files_to_copy += 1
607.
608. for source in source_dirs:
609. self.assign_targets(source, target)
610.
611. self.progress("targets assigned, %s dirs, %s files" %
612. (len(self.targetmap), self.files_to_copy))
613.
614. self.progress("starting copy, %d files, %d directories" %
615. (self.files_to_copy, len(self.targetmap)))
616. self.files_copied = 0
617. self.targets_finished = 0
618.
619. # step four: walk through the list of targets. For each one, copy all
620. # the files. If the target is a TahoeDirectory, upload and create
621. # read-caps, then do a set_children to the target directory.
622.
623. for target in self.targetmap:
624. self.copy_files_to_target(self.targetmap[target], target)
625. self.targets_finished += 1
626. self.progress("%d/%d directories" %
627. (self.targets_finished, len(self.targetmap)))
628.
629. def attach_to_target(self, source, name, target):
630. if target not in self.targetmap:
631. self.targetmap[target] = {}
632. self.targetmap[target][name] = source
633. self.files_to_copy += 1
634.
635. def assign_targets(self, source, target):
636. # copy everything in the source into the target
637. assert isinstance(source, (LocalDirectorySource, TahoeDirectorySource))
638.
639. for name, child in source.children.items():
640. if isinstance(child, (LocalDirectorySource, TahoeDirectorySource)):
641. # we will need a target directory for this one
642. subtarget = target.get_child_target(name)
643. self.assign_targets(child, subtarget)
644. else:
645. assert isinstance(child, (LocalFileSource, TahoeFileSource))
646. self.attach_to_target(child, name, target)
647.
648.
649.
650. def copy_files_to_target(self, targetmap, target):
651. for name, source in targetmap.items():
652. assert isinstance(source, (LocalFileSource, TahoeFileSource))
653. self.copy_file_into(source, name, target)
654. self.files_copied += 1
655. self.progress("%d/%d files, %d/%d directories" %
656. (self.files_copied, self.files_to_copy,
657. self.targets_finished, len(self.targetmap)))
658. target.set_children()
659.
660. def need_to_copy_bytes(self, source, target):
661. if source.need_to_copy_bytes:
662. # mutable tahoe files, and local files
663. return True
664. if isinstance(target, (LocalFileTarget, LocalDirectoryTarget)):
665. return True
666. return False
667.
668. def copy_file(self, source, target):
669. assert isinstance(source, (LocalFileSource, TahoeFileSource))
670. assert isinstance(target, (LocalFileTarget, TahoeFileTarget,
671. LocalMissingTarget, TahoeMissingTarget))
672. if self.need_to_copy_bytes(source, target):
673. # if the target is a local directory, this will just write the
674. # bytes to disk. If it is a tahoe directory, it will upload the
675. # data, and stash the new filecap for a later set_children call.
676. f = source.open()
677. target.put_file(f)
678. return
679. # otherwise we're copying tahoe to tahoe, and using immutable files,
680. # so we can just make a link. TODO: this probably won't always work:
681. # need to enumerate the cases and analyze them.
682. target.put_uri(source.bestcap())
683.
684. def copy_file_into(self, source, name, target):
685. assert isinstance(source, (LocalFileSource, TahoeFileSource))
686. assert isinstance(target, (LocalDirectoryTarget, TahoeDirectoryTarget))
687. if self.need_to_copy_bytes(source, target):
688. # if the target is a local directory, this will just write the
689. # bytes to disk. If it is a tahoe directory, it will upload the
690. # data, and stash the new filecap for a later set_children call.
691. f = source.open()
692. target.put_file(name, f)
693. return
694. # otherwise we're copying tahoe to tahoe, and using immutable files,
695. # so we can just make a link
696. target.put_uri(name, source.bestcap())
697.
698.
699. def progress(self, message):
700. #print message
701. if self.progressfunc:
702. self.progressfunc(message)
703.
704. def build_graphs(self, source_infos):
705. graphs = []
706. for name,source in source_infos:
707. if isinstance(source, (LocalDirectorySource, TahoeDirectorySource)):
708. source.populate(True)
709. graphs.append(source)
710. return graphs
711.
712.
713. def copy(options):
714. return Copier().do_copy(options)