1#!/usr/bin/python
2
3###############################################################
4# This program creates a command line interpreter used to edit
5# kippo file system pickle files.
6#
7# It is intended to mimic a basic bash shell and supports relative
8# file references.
9#
10# This isn't meant to build a brand new filesystem. Instead it
11# should be used to edit existing filesystems such as the default
12# /opt/kippo/fs.pickle.
13#
14# Donovan Hubbard
15# Douglas Hubbard
16# March 2013
17#
18###############################################################
19
20import os, pickle, sys, locale, time, cmd
21from stat import *
22
23A_NAME, A_TYPE, A_UID, A_GID, A_SIZE, A_MODE, \
24    A_CTIME, A_CONTENTS, A_TARGET, A_REALFILE = range(0, 10)
25T_LINK, T_DIR, T_FILE, T_BLK, T_CHR, T_SOCK, T_FIFO = range(0, 7)
26
27def getpath(fs, path):
28    cwd = fs
29    for part in path.split('/'):
30        if not len(part):
31            continue
32        ok = False
33        for c in cwd[A_CONTENTS]:
34            if c[A_NAME] == part:
35                cwd = c
36                ok = True
37                break
38        if not ok:
39            raise Exception('File not found')
40    return cwd
41
42def exists(fs, path):
43    try:
44        getpath(fs, path)
45        return True
46    except Exception, e:
47        if str(e) == 'File not found':
48            return False
49        else:
50            raise Exception(e)
51
52def is_directory(fs,path):
53    "Returns whether or not the file at 'path' is a directory"
54    file = getpath(fs,path)
55    if file[A_TYPE] == T_DIR:
56        return True
57    else:
58        return False
59
60def resolve_reference(pwd, relativeReference):
61    '''Used to resolve a current working directory and a relative
62      reference into an absolute file reference.'''
63
64    tempPath = os.path.join(pwd, relativeReference)
65    absoluteReference = os.path.normpath(tempPath)
66
67    return absoluteReference
68
69class fseditCmd(cmd.Cmd):
70
71    def __init__(self, pickle_file_path):
72        cmd.Cmd.__init__(self)
73
74        if not os.path.isfile(pickle_file_path):
75            print "File %s does not exist." % pickle_file_path
76            sys.exit(1)
77
78        try:
79            pickle_file = open(pickle_file_path, 'rb')
80        except IOError as e:
81            print "Unable to open file %s" % pickle_file_path
82            sys.exit(1)
83
84        try:
85            self.fs = pickle.load(pickle_file)
86        except:
87            print ("Unable to load file '%s'. " + \
88                "Are you sure it is a valid pickle file?") % \
89                (pickle_file_path,)
90            sys.exit(1)
91
92        self.pickle_file_path=pickle_file_path
93
94        #get the name of the file so we can display it as the prompt
95        path_parts = pickle_file_path.split('/')
96        self.fs_name = path_parts[-1]
97
98        self.update_pwd("/")
99
100        self.intro = "\nKippo file system interactive editor\n" + \
101            "Donovan Hubbard, Douglas Hubbard, March 2013\n" + \
102            "Type 'help' for help\n"
103
104    def save_pickle(self):
105        '''saves the current file system to the pickle'''
106        try:
107            pickle.dump(self.fs, file(self.pickle_file_path, 'wb'))
108        except:
109            print ("Unable to save pickle file '%s'. " + \
110                "Are you sure you have write access?") % \
111                (self.pickle_file_path,)
112            sys.exit(1)
113
114    def do_exit(self, args):
115        '''Exits the file system editor'''
116        return True
117
118    def do_EOF(self, args):
119        '''The escape character ctrl+d exits the session'''
120        #exiting from the do_EOF method does not create a newline automaticaly
121        #so we add it manually
122        print
123        return True
124
125    def do_ls(self, args):
126        '''Prints the contents of a directory.
127        Prints the current directory if no arguments are specified'''
128
129        if not len(args):
130            path = self.pwd
131        else:
132            path = resolve_reference(self.pwd,args)
133
134        if exists(self.fs, path) == False:
135            print "ls: cannot access %s: No such file or directory" % (path,)
136            return
137
138        if is_directory(self.fs, path) == False:
139            print "ls: %s is not a directory" % (path,)
140            return
141
142        cwd = getpath(self.fs, path)
143
144        for file in cwd[A_CONTENTS]:
145            if file[A_TYPE] == T_DIR:
146                print file[A_NAME] + '/'
147            else:
148                print file[A_NAME]
149
150    def update_pwd(self, directory):
151        self.pwd = directory
152        self.prompt = self.fs_name + ":" + self.pwd + "$ "
153
154    def do_cd(self, args):
155        '''Changes the current directory.\nUsage: cd <target directory>'''
156
157        #count  the number of arguments
158        # 1 or more arguments: changes the directory to the first arg
159        #                      and ignores the rest
160        # 0 arguments: changes to '/'
161        arguments = args.split()
162
163        if not len(arguments):
164            self.update_pwd("/")
165        else:
166            relative_dir = arguments[0]
167            target_dir = resolve_reference(self.pwd, relative_dir)
168
169            if exists(self.fs, target_dir) == False:
170                print "cd: %s: No such file or directory" %  target_dir
171            elif is_directory(self.fs, target_dir):
172                self.update_pwd(target_dir)
173            else:
174                print "cd: %s: Not a directory" % target_dir
175
176    def do_pwd(self, args):
177        '''Prints the current working directory'''
178        print self.pwd
179
180    def do_mkdir(self, args):
181        """Add a new directory in the target directory.
182        Handles relative or absolute file paths. \n
183        Usage: mkdir <destination>"""
184
185        arg_list=args.split()
186        if len(arg_list) != 1:
187            print "usage: mkdir <new directory>"
188        else:
189            self.mkfile(arg_list, T_DIR)
190
191    def do_touch(self, args):
192        """Add a new file in the target directory.
193        Handles relative or absolute file paths. \n
194        Usage: touch <destination> [<size in bytes>]"""
195
196        arg_list=args.split()
197
198        if len(arg_list) < 1:
199            print 'Usage: touch <destination> (<size in bytes>)'
200        else:
201            self.mkfile(arg_list, T_FILE)
202
203    def mkfile(self, args, file_type):
204        '''args must be a list of arguments'''
205        cwd = self.fs
206        path = resolve_reference(self.pwd, args[0])
207        pathList = path.split('/')
208        parentdir = '/'.join(pathList[:-1])
209        fileName = pathList[len(pathList) - 1]
210
211        if not exists(self.fs, parentdir):
212            print ('Parent directory %s doesn\'t exist! ' +
213                'Please create it first.') % \
214                (parentdir,)
215            return
216
217        if exists(self.fs, path):
218            print 'Error: %s already exists!' % (path,)
219            return
220
221        cwd = getpath(self.fs, parentdir)
222
223        #get uid, gid, mode from parent
224        uid = cwd[A_UID]
225        gid = cwd[A_GID]
226        mode = cwd[A_MODE]
227
228        #create default file/directory size if none is specified
229        if len(args) == 1:
230            size = 4096
231        else:
232            size = args[1]
233
234        #set the last update timestamp to now
235        ctime = time.time()
236
237        cwd[A_CONTENTS].append(
238            [fileName, file_type, uid, gid, size, mode, ctime, [], None, None])
239
240        self.save_pickle()
241
242        print "Added '%s'" % path
243
244    def do_rm(self, arguments):
245        '''Remove an object from the filesystem.
246        Will not remove a directory unless the -r switch is invoked.\n
247        Usage: rm [-r] <target>'''
248
249        args = arguments.split()
250
251        if len(args) < 1 or len(args) > 2:
252            print 'Usage: rm [-r] <target>'
253            return
254
255        if len(args) == 2 and args[0] != "-r":
256            print 'Usage: rm [-r] <target>'
257            return
258
259        if len(args) == 1:
260            target_path = resolve_reference(self.pwd, args[0])
261        else:
262            target_path = resolve_reference(self.pwd, args[1])
263
264        if exists(self.fs, target_path) == False:
265            print "File \'%s\' doesn\'t exist" % (target_path,)
266            return
267
268        if target_path == "/":
269            print "rm: cannot delete root directory '/'"
270            return
271
272        target_object = getpath(self.fs, target_path)
273
274        if target_object[A_TYPE]==T_DIR and args[0] != "-r":
275            print "rm: cannot remove '%s': Is a directory" % (target_path,)
276            return
277
278        parent_path = '/'.join(target_path.split('/')[:-1])
279        parent_object = getpath(self.fs, parent_path)
280
281        parent_object[A_CONTENTS].remove(target_object)
282
283        self.save_pickle()
284
285        print "Deleted %s" % target_path
286
287    def do_rmdir(self, arguments):
288        '''Remove a file object. Like the unix command,
289        this can only delete empty directories.
290        Use rm -r to recursively delete full directories.\n
291        Usage: rmdir <target directory>'''
292        args = arguments.split()
293
294        if len(args) != 1:
295            print 'Usage: rmdir <target>'
296            return
297
298        target_path = resolve_reference(self.pwd, args[0])
299
300        if exists(self.fs, target_path) == False:
301            print "File \'%s\' doesn\'t exist" % (target_path,)
302            return
303
304        target_object = getpath(self.fs, target_path)
305
306        if target_object[A_TYPE] != T_DIR:
307            print "rmdir: failed to remove '%s': Not a directory" % \
308                (target_path,)
309            return
310
311        #The unix rmdir command does not delete directories if they are not
312        #empty
313        if len(target_object[A_CONTENTS]) != 0:
314            print "rmdir: failed to remove '%s': Directory not empty" % \
315                (target_path,)
316            return
317
318        parent_path = '/'.join(target_path.split('/')[:-1])
319        parent_object = getpath(self.fs, parent_path)
320
321        parent_object[A_CONTENTS].remove(target_object)
322
323        self.save_pickle()
324
325        if self.pwd == target_path:
326           self.do_cd("..")
327
328        print "Deleted %s" % target_path
329
330    def do_mv(self, arguments):
331        '''Moves a file/directory from one directory to another.\n
332        Usage: mv <source file> <destination file>'''
333        args = arguments.split()
334        if len(args) != 2:
335            print 'Usage: mv <source> <destination>'
336            return
337        src = resolve_reference(self.pwd, args[0])
338        dst = resolve_reference(self.pwd, args[1])
339
340        if src == "/":
341           print "mv: cannot move the root directory '/'"
342           return
343
344        src = src.strip('/')
345        dst = dst.strip('/')
346
347        if not exists(self.fs, src):
348           print "Source file \'%s\' does not exist!" % src
349           return
350
351        #Get the parent directory of the source file
352        #srcparent = '/'.join(src.split('/')[:-1])
353        srcparent = "/".join(src.split('/')[:-1])
354
355        #Get the object for source
356        srcl = getpath(self.fs, src)
357
358        #Get the object for the source's parent
359        srcparentl = getpath(self.fs, srcparent)
360
361        #if the specified filepath is a directory, maintain the current name
362        if exists(self.fs, dst) and is_directory(self.fs, dst):
363            dstparent = dst
364            dstname = srcl[A_NAME]
365        else:
366            dstparent = '/'.join(dst.split('/')[:-1])
367            dstname = dst.split('/')[-1]
368
369        if exists(self.fs, dstparent + '/' + dstname):
370            print "A file already exists at "+dst+"!"
371            return
372
373        if not exists(self.fs, dstparent):
374            print 'Destination directory \'%s\' doesn\'t exist!' % dst
375            return
376
377        if src == self.pwd:
378            self.do_cd("..")
379
380        dstparentl = getpath(self.fs, dstparent)
381        copy = srcl[:]
382        copy[A_NAME] = dstname
383        dstparentl[A_CONTENTS].append(copy)
384        srcparentl[A_CONTENTS].remove(srcl)
385
386        self.save_pickle()
387
388        print 'File moved from /%s to /%s' % (src, dst)
389
390    def do_cp(self, arguments):
391        '''Copies a file/directory from one directory to another.\n
392        Usage: cp <source file> <destination file>'''
393        args = arguments.split()
394        if len(args) != 2:
395            print 'Usage: cp <source> <destination>'
396            return
397
398        #src, dst = args[0], args[1]
399
400        src = resolve_reference(self.pwd, args[0])
401        dst = resolve_reference(self.pwd, args[1])
402
403        src = src.strip('/')
404        dst = dst.strip('/')
405
406        if not exists(self.fs, src):
407           print "Source file '%s' does not exist!" % (src,)
408           return
409
410        #Get the parent directory of the source file
411        srcparent = '/'.join(src.split('/')[:-1])
412
413        #Get the object for source
414        srcl = getpath(self.fs, src)
415
416        #Get the ojbect for the source's parent
417        srcparentl = getpath(self.fs, srcparent)
418
419        #if the specified filepath is a directory, maintain the current name
420        if exists(self.fs, dst) and is_directory(self.fs, dst):
421            dstparent = dst
422            dstname = srcl[A_NAME]
423        else:
424            dstparent = '/'.join(dst.split('/')[:-1])
425            dstname = dst.split('/')[-1]
426
427        if exists(self.fs, dstparent + '/' + dstname):
428            print 'A file already exists at %s/%s!' % (dstparent, dstname)
429            return
430
431        if not exists(self.fs, dstparent):
432            print 'Destination directory %s doesn\'t exist!' % (dstparent,)
433            return
434
435        dstparentl = getpath(self.fs, dstparent)
436        copy = srcl[:]
437        copy[A_NAME] = dstname
438        dstparentl[A_CONTENTS].append(copy)
439
440        self.save_pickle()
441
442        print 'File copied from /%s to /%s/%s' % (src, dstparent, dstname)
443
444    def do_file(self, args):
445        '''Identifies file types.\nUsage: file <file name>'''
446        arg_list = args.split()
447
448        if len(arg_list) != 1:
449            print "Incorrect number of arguments.\nUsage: file <file>"
450            return
451
452        target_path = resolve_reference(self.pwd, arg_list[0])
453
454        if not exists(self.fs, target_path):
455            print "File '%s' doesn't exist." % target_path
456            return
457
458        target_object = getpath(self.fs, target_path)
459
460        file_type = target_object[A_TYPE]
461
462        if file_type == T_FILE:
463            msg = "normal file object"
464        elif file_type == T_DIR:
465            msg = "directory"
466        elif file_type == T_LINK:
467            msg = "link"
468        elif file_type == T_BLK:
469            msg = "block file"
470        elif file_type == T_CHR:
471            msg = "character special"
472        elif file_type == T_SOCK:
473            msg = "socket"
474        elif file_type == T_FIFO:
475            msg = "named pipe"
476        else:
477            msg = "unrecognized file"
478
479        print target_path+" is a "+msg
480
481    def do_clear(self, args):
482        '''Clears the screen'''
483        os.system('clear')
484
485    def emptyline(self):
486        '''By default the cmd object will repeat the last command
487        if a blank line is entered. Since this is different than
488        bash behavior, overriding this method will stop it.'''
489        pass
490
491    def help_help(self):
492        print "Type help <topic> to get more information."
493
494    def help_about(self):
495        print "Kippo stores information about its file systems in a " + \
496            "series of nested lists. Once the lists are made, they are " + \
497            "stored in a pickle file on the hard drive. Every time kippo " + \
498            "gets a new client, it reads from the pickle file and loads " + \
499            "the fake filesystem into memory. By default this file " + \
500            "is /opt/kippo/fs.pickle. Originally the script " + \
501            "/opt/kippo/createfs.py was used to copy the filesystem " + \
502            "of the existing computer. However, it quite difficult to " + \
503            "edit the pickle file by hand.\n\nThis script strives to be " + \
504            "a bash-like interface that allows users to modify " + \
505            "existing fs pickle files. It supports many of the " + \
506            "common bash commands and even handles relative file " + \
507            "paths. Keep in mind that you need to restart the " + \
508            "kippo process in order for the new file system to be " + \
509            "reloaded into memory.\n\nDonovan Hubbard, Douglas Hubbard, " + \
510            "March 2013\nVersion 1.0"
511
512if __name__ == '__main__':
513    if len(sys.argv) != 2:
514        print "Usage: %s <fs.pickle>" % os.path.basename(sys.argv[0],)
515        sys.exit(1)
516
517    pickle_file_name = sys.argv[1].strip()
518    print pickle_file_name
519
520    fseditCmd(pickle_file_name).cmdloop()
521
522# vim: set sw=4 et:
523