forked from lethain/aym-cms
/
file_system.py
601 lines (478 loc) · 16.9 KB
/
file_system.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
"""
Unified interface for performing file system tasks. Uses os, os.path. shutil
and distutil to perform the tasks. The behavior of some functions is slightly
contaminated with requirements from Hyde: For example, the backup function
deletes the directory that is being backed up.
"""
import os
import shutil
import codecs
import fnmatch
from datetime import datetime
from distutils import dir_util, file_util
from path_util import PathUtil
class FileSystemEntity(object):
"""
Base class for files and folders.
"""
def __init__(self, path):
super(FileSystemEntity, self).__init__()
if path is FileSystemEntity:
self.path = path.path
else:
self.path = path
def __str__(self):
return self.path
def __repr__(self):
return self.path
def allow(self, include=None, exclude=None):
"""
Given a set of wilcard patterns in the include and exclude arguments,
tests if the patterns allow this item for processing.
The exclude parameter is processed first as a broader filter and then
include is used as a narrower filter to override the results for more
specific files.
Example:
exclude = (".*", "*~")
include = (".htaccess")
"""
if not include:
include = ()
if not exclude:
exclude = ()
if reduce(lambda result,
pattern: result or
fnmatch.fnmatch(self.name, pattern), include, False):
return True
if reduce(lambda result, pattern:
result and not fnmatch.fnmatch(self.name, pattern),
exclude, True):
return True
return False
@property
def humblepath(self):
"""
Expands variables, user, normalizes path and case and coverts
to absolute.
"""
return os.path.abspath(
os.path.normpath(
os.path.normcase(
os.path.expandvars(
os.path.expanduser(self.path)))))
def same_as(self, other):
"""
Checks if the path of this object is same as `other`. `other` must
be a FileSystemEntity.
"""
return (self.humblepath.rstrip(os.sep) ==
other.humblepath.rstrip(os.sep))
@property
def exists(self):
"""
Checks if the entity exists in the file system.
"""
return os.path.exists(self.path)
@property
def isdir(self):
"""
Is this a folder.
"""
return os.path.isdir(self.path)
@property
def stats(self):
"""
Shortcut for os.stat.
"""
return os.stat(self.path)
@property
def name(self):
"""
Name of the entity. Calls os.path.basename.
"""
return os.path.basename(self.path)
@property
def parent(self):
"""
The parent folder. Returns a `Folder` object.
"""
return Folder(os.path.dirname(self.path))
def __get_destination__(self, destination):
"""
Returns a File or Folder object that would represent this entity
if it were copied or moved to `destination`. `destination` must be
an instance of File or Folder.
"""
if os.path.isdir(str(destination)):
target = destination.child(self.name)
if os.path.isdir(self.path):
return Folder(target)
else: return File(target)
else:
return destination
# pylint: disable-msg=R0904,W0142
class File(FileSystemEntity):
"""
Encapsulates commonly used functions related to files.
"""
def __init__(self, path):
super(File, self).__init__(path)
@property
def size(self):
"""
Gets the file size
"""
return os.path.getsize(self.path)
#return 1
def has_extension(self, extension):
"""
Checks if this file has the given extension.
"""
return self.extension == extension
def delete(self):
"""
Deletes if the file exists.
"""
if self.exists:
os.remove(self.path)
@property
def last_modified(self):
"""
Returns a datetime object representing the last modified time.
Calls os.path.getmtime.
"""
return datetime.fromtimestamp(os.path.getmtime(self.path))
def changed_since(self, basetime):
"""
Returns True if the file has been changed since the given time.
"""
return self.last_modified > basetime
def older_than(self, another_file):
"""
Checks if this file is older than the given file. Uses last_modified to
determine age.
"""
return another_file.last_modified > self.last_modified
@property
def path_without_extension(self):
"""
The full path of the file without its extension.
"""
return os.path.splitext(self.path)[0]
@property
def name_without_extension(self):
"""
Name of the file without its extension.
"""
return os.path.splitext(self.name)[0]
@property
def extension(self):
"""
File's extension prefixed with a dot.
"""
return os.path.splitext(self.path)[1]
@property
def kind(self):
"""
File's extension without a dot prefix.
"""
return self.extension.lstrip(".")
def move_to(self, destination):
"""
Moves the file to the given destination. Returns a File
object that represents the target file. `destination` must
be a File or Folder object.
"""
shutil.move(self.path, str(destination))
return self.__get_destination__(destination)
def copy_to(self, destination):
"""
Copies the file to the given destination. Returns a File
object that represents the target file. `destination` must
be a File or Folder object.
"""
shutil.copy(self.path, str(destination))
return self.__get_destination__(destination)
def write(self, text, encoding="utf-8"):
"""
Writes the given text to the file using the given encoding.
"""
fout = codecs.open(self.path, 'w', encoding)
fout.write(text)
fout.close()
def read_all(self):
"""
Reads from the file and returns the content as a string.
"""
fin = codecs.open(self.path, 'r')
read_text = fin.read()
fin.close()
return read_text
# pylint: disable-msg=R0904,W0142
class Folder(FileSystemEntity):
"""
Encapsulates commonly used directory functions.
"""
def __init__(self, path):
super(Folder, self).__init__(path)
def __str__(self):
return self.path
def __repr__(self):
return self.path
def delete(self):
"""
Deletes the directory if it exists.
"""
if self.exists:
shutil.rmtree(self.path)
def depth(self):
"""
Returns the number of ancestors of this directory.
"""
return len(self.path.split(os.sep))
def make(self):
"""
Creates this directory and any of the missing directories in the path.
Any errors that may occur are eaten.
"""
try:
if not self.exists:
os.makedirs(self.path)
except:
pass
return self
def is_parent_of(self, other_entity):
"""
Returns True if this directory is a direct parent of the the given
directory.
"""
return self.same_as(other_entity.parent)
def is_ancestor_of(self, other_entity):
"""
Returns True if this directory is in the path of the given directory.
Note that this will return True if the given directory is same as this.
"""
folder = other_entity
while not folder.parent.same_as(folder):
folder = folder.parent
if self.same_as(folder):
return True
return False
def child(self, name):
"""
Returns a path of a child item represented by `name`.
"""
return os.path.join(self.path, name)
def child_folder(self, *args):
"""
Returns a Folder object by joining the path component in args
to this directory's path.
"""
return Folder(os.path.join(self.path, *args))
def child_folder_with_fragment(self, fragment):
"""
Returns a Folder object by joining the fragment to
this directory's path.
"""
return Folder(os.path.join(self.path, fragment.lstrip(os.sep)))
def get_fragment(self, root):
"""
Returns the path fragment of this directory starting with the given
directory.
"""
return PathUtil.get_path_fragment(str(root), self.path)
def get_mirror_folder(self, root, mirror_root, ignore_root=False):
"""
Returns a Folder object that reperesents if the entire fragment of this
directory starting with `root` were copied to `mirror_root`. If ignore_root
is True, the mirror does not include `root` directory itself.
Example:
Current Directory: /usr/local/hyde/stuff
root: /usr/local/hyde
mirror_root: /usr/tmp
Result:
if ignore_root == False:
Folder(/usr/tmp/hyde/stuff)
if ignore_root == True:
Folder(/usr/tmp/stuff)
"""
path = PathUtil.get_mirror_dir(
self.path, str(root), str(mirror_root), ignore_root)
return Folder(path)
def create_mirror_folder(self, root, mirror_root, ignore_root=False):
"""
Creates the mirror directory returned by `get_mirror_folder`
"""
mirror_folder = self.get_mirror_folder(
root, mirror_root, ignore_root)
mirror_folder.make()
return mirror_folder
def backup(self, destination):
"""
Creates a backup of this directory in the given destination. The backup is
suffixed with a number for uniqueness. Deletes this directory after backup
is complete.
"""
new_name = self.name
count = 0
dest = Folder(destination.child(new_name))
while(True):
dest = Folder(destination.child(new_name))
if not dest.exists:
break
else:
count = count + 1
new_name = self.name + str(count)
dest.make()
dest.move_contents_of(self)
self.delete()
return dest
def move_to(self, destination):
"""
Moves this directory to the given destination. Returns a Folder object
that represents the moved directory.
"""
shutil.copytree(self.path, str(destination))
shutil.rmtree(self.path)
return self.__get_destination__(destination)
def copy_to(self, destination):
"""
Copies this directory to the given destination. Returns a Folder object
that represents the moved directory.
"""
shutil.copytree(self.path, str(destination))
return self.__get_destination__(destination)
def move_folder_from(self, source, incremental=False):
"""
Moves the given source directory to this directory. If incremental is True
only newer objects are overwritten.
"""
self.copy_folder_from(source, incremental)
shutil.rmtree(str(source))
def copy_folder_from(self, source, incremental=False):
"""
Copies the given source directory to this directory. If incremental is True
only newer objects are overwritten.
"""
# There is a bug in dir_util that makes copy_tree crash if a folder in the
# tree has been deleted before and readded now. To workaround the bug, we first
# walk the tree and create directories that are needed.
#
# pylint: disable-msg=C0111,W0232
target_root = self
class _DirCreator:
@staticmethod
def visit_folder(folder):
target = folder.get_mirror_folder(
source.parent, target_root, ignore_root=True)
target.make()
source.walk(_DirCreator)
dir_util.copy_tree(str(source),
self.child(source.name),
update=incremental)
def move_contents_of(self, source, move_empty_folders=True,
incremental=False):
"""
Moves the contents of the given source directory to this directory. If
incremental is True only newer objects are overwritten.
"""
# pylint: disable-msg=C0111,W0232
class _Mover:
@staticmethod
def visit_folder(folder):
self.move_folder_from(folder, incremental)
@staticmethod
def visit_file(a_file):
self.move_file_from(a_file, incremental)
source.list(_Mover, move_empty_folders)
def copy_contents_of(self, source, copy_empty_folders=True,
incremental=False):
"""
Copies the contents of the given source directory to this directory. If
incremental is True only newer objects are overwritten.
"""
# pylint: disable-msg=C0111,W0232
class _Copier:
@staticmethod
def visit_folder(folder):
self.copy_folder_from(folder, incremental)
@staticmethod
def visit_file(a_file):
self.copy_file_from(a_file, incremental)
source.list(_Copier, copy_empty_folders)
def move_file_from(self, source, incremental=False):
"""
Moves the given source file to this directory. If incremental is True the
move is performed only if the source file is newer.
"""
self.copy_file_from(source, incremental)
source.delete()
def copy_file_from(self, source, incremental=False):
"""
Copies the given source file to this directory. If incremental is True the
move is performed only if the source file is newer.
"""
file_util.copy_file(str(source), self.path, update=incremental)
def list(self, visitor, list_empty_folders=True):
"""
Calls the visitor.visit_file or visitor.visit_folder for each file or folder
in this directory. If list_empty_folders is False folders that are empty are
skipped.
"""
a_files = os.listdir(self.path)
for a_file in a_files:
path = os.path.join(self.path, str(a_file))
if os.path.isdir(path):
if not list_empty_folders:
if Folder(path).empty():
continue
visitor.visit_folder(Folder(path))
else:
visitor.visit_file(File(path))
def empty(self):
"""
Checks if this directory or any of its subdirectories contain files.
"""
paths = os.listdir(self.path)
for path in paths:
if os.path.isdir(path):
if not Folder(path).empty():
return False
else:
return False
return True
def walk(self, visitor = None, pattern = None):
"""
Walks the entire hirearchy of this directory starting with itself.
Calls visitor.visit_folder first and then calls visitor.visit_file for
any files found. After all files and folders have been exhausted
visitor.visit_complete is called.
If a pattern is provided, only the files that match the pattern are
processed.
If visitor.visit_folder returns False, the files in the folder are not
processed.
"""
def __visit_folder__(visitor, folder):
process_folder = True
if visitor and hasattr(visitor,'visit_folder'):
process_folder = visitor.visit_folder(folder)
# If there is no return value assume true
#
if process_folder is None:
process_folder = True
return process_folder
def __visit_file__(visitor, a_file):
if visitor and hasattr(visitor,'visit_file'):
visitor.visit_file(a_file)
def __visit_complete__(visitor):
if visitor and hasattr(visitor,'visit_complete'):
visitor.visit_complete()
for root, dirs, a_files in os.walk(self.path):
folder = Folder(root)
if not __visit_folder__(visitor, folder):
dirs[:] = []
continue
for a_file in a_files:
if not pattern or fnmatch.fnmatch(a_file, pattern):
__visit_file__(visitor, File(folder.child(a_file)))
__visit_complete__(visitor)