Skip to main content

OOP - Build a Filesystem with the „Composite Pattern“

· 8 min read
Strider

Hi, I have bock to tinker a small file system. A filesystem is in the end, a tree. Normally a B-tree or a (B+)-tree is used to realize a filesystem. I thought why not make the whole thing OOP.

A file system consists of folders and files. We have a folder that contains files and folders. These in turn also contain files and folders.

/
├── etc/
│ ├── ...
│ ├── hosts
│ └── init.d/
│ ├── ...
│ └── service.sh
├── ...
├── home
├── ...
├── root
└── ...

You can see that there is a hierarchy here. You can continue this hierarchy endlessly. But the question is, how do you implement that with little code? That's the right question, because that's where the composite pattern comes in.

Briefly, the Composite pattern is a design pattern from Object Oriented Programming. This pattern was designed for hierarchical structures e.g. employee hierarchy in a company or military etc....

The pattern is perfectly suitable for such implementations. But before using the pattern you should check if the core of the application supports this structure at all.

A small UML shows how the composite pattern is structured.

dia.png

We see the abstract class Component. This class is the basis of the pattern, because the File and Directory classes inherit from Component to make the pattern work.

You can also say that a file is a component of the file system. The same is true for folders.

The class Directory, can contain files and additional folders. This is represented as aggregation.

The class Component, still has the public method execute defined as a prototype, which is implemented differently in both classes. This method is to simply output the contents of the files.

Implement that "filesystem"

As an example, I want to build a Linux-like file system. Well from the structure. The structure should look like this:

/
├── initrd.img
├── vmlinuz
├── etc/
│ ├── hosts
│ └── resolv.conf
└── tmp/
├── 3649873242
└── dsfdsfdsf

First, the abstract class Component should be implemented. The implementation is done in Python.

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import abc

class Component(object):
__metaclass__ = abc.ABCMeta

@abc.abstractmethod
def execute(self):
raise Exception("Not implemented error")

If we look at the code, we see that we have only implemented an empty class with the prototype method execute. The File class, on the other hand, looks a bit more lively.

Implement the File

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from Component import Component

class File(Component):

__filename = None
__filesize = None
__filetype = None
__filecontent = None

def __init__(self, name, size, type, content):
self.__filename = name
self.__filesize = size
self.__filetype = type
self.__filecontent = content

def execute(self):
print('\n\n[CONTENT OF FILE ' + self.__filename + ']')
print(self.getContent())

def getFileName(self):
return self.__filename

def getFileSize(self):
return self.__filesize

def getFileType(self):
return self.__filetype

def getContent(self):
return self.__filecontent

def setFileName(self, name):
self.__filename = name

def setFileSize(self, size):
self.__filesize = size

def setFileType(self, type):
self.__filetype = type

def setContent(self, content):
self.__filecontent = content

We see here that our class File inherits from the class Component. Thus the class File is also of the type Component. Furthermore we see that four private attributes are defined.

Because we need a file name __filename , a file size __filesize, the file type __filetype and the content of the file __filecontent. The execute method, simply calls the getter getContent, and prints the content to the console.

The constructor, gets all four file information defined as parameters, which are filled with data when instantiated. For each attribute, getters and setters are defined.

Implement the Directory

The Directory class is the largest class of all in the implementation.

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from Component import Component
from File import File

class Directory(Component):
__objects = None
__name = None

def __init__(self, name):
self.__objects = []
self.__name = name

def getName(self):
return self.__name

def setName(self, name):
self.__name = name

def execute(self):
for obj in self.__objects:
obj.execute()

def add(self, obj):
if not obj in self.__objects:
self.__objects.append(obj)

def remove(self, obj):
if obj in self.__objects:
self.__objects.remove(obj)

def list(self):
print(self.getName())
for obj in self.__objects:
if isinstance(obj, File):
print("|- " + obj.getFileName() + "\t" + obj.getFileSize() + "\t" + obj.getFileType())

else:
print(obj.getName()+'/')

def listAll(self):
print(self.getName())
for obj in self.__objects:
if isinstance(obj, File):
print("|- " + obj.getFileName() + "\t" + obj.getFileSize() + "\t" + obj.getFileType())

else:
print("Directory: " + obj.getName())
obj.list()

def search(self, file):
results = []
for obj in self.__objects:
if isinstance(obj, File):
if obj.getFileName().__contains__(file):
results.append(obj)

else:
if obj.getName().__contains__(file):
results.append(obj)

for r in obj.search(file):
results.append(r)

return results

def getChild(self, file):
for obj in self.__objects:
if isinstance(obj, File):
if obj.getFileName() == file:
return obj
else:
if obj.getName() == file:
return obj

We see here that the Directory class inherits from the Component class. This means that all methods must be implemented here as well. More about this later. The class has two private attributes, once the list objects, which later contains component instances, no matter if file or folder. And once the name, because folders also have names.

Both are initialized in the constructor. The method execute, goes through all objects in the list objects, and calls the method execute for each object.

The method add, adds a file or a directory to the list objects. But only if this object is not already in the list. The method remove removes the object.

The method list, should give us the list of files and directories in the current object. To do this, it wanders over the list and checks whether it is of type File or Directory. Depending on the type, appropriate getters are called.

The listAll method, on the other hand, lists not only everything in the current object, but also in the subfolder objects.

A search must also not be missing, which is why we include the method search. Here, using the parameter file, each object in the list is checked for matches.

A list is created, which later stores our search results. All objects are searched and their names are retrieved. If our string occurs in the name, the type of the object is checked. If the current object is of type File, we simply add it to the results. Otherwise, if it is a folder, the same, but call the method search of the subfolder. At the end, the search results are returned.

To select from a folder, files or subfolders, there is the method getChild. Here the list is traversed again until there is an exact match. If that is the case, the object in question is returned.

Build the filesystem

Now we build with it, the file system from above.

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from File import File
from Directory import Directory

if __name__ == "__main__":
root = Directory('/')
for f in [('initrd.img', '36', 'img', 'init 1'), ('vmlinuz', '32', 'file', '#!/bin/sh\nexec /boot/vmlinuz')]:
root.add(File(f[0], f[1], f[2], f[3]))

print("\n\nListing /")
root.list()
etc = Directory('/etc')
for f in [('hosts', '255', 'file', '127.0.0.1\tlocalhost'), ('resov.conf', '255', 'file', 'nameserver 8.8.8.8')]:
etc.add(File(f[0], f[1], f[2], f[3]))

root.add(etc)

print("\n\nListing all in /")
root.listAll()

tmp = Directory('/tmp')
for f in [('3649873242', '255', 'file', 'fdsfdsfs'), ('dsfdsfdsf', '255', 'file', 'fdsfdsfs')]:
tmp.add(File(f[0], f[1], f[2], f[3]))
tmp.execute()

root.add(tmp)
root.listAll()
root.execute()
tmp = root.getChild('/tmp')
file = tmp.getChild('3649873242')
file.setFileName('abc')
tmp.list()
tmp.remove(file)
root.listAll()
print('\n\nSearching files and dirs containing e...')
res = root.search('e')
for r in res:
if isinstance(r, File):
print(r.getFileName())

else:
print(r.getName())

Here we simply build the structure from above and test everything. The output looks like this:

Listing /
/
|- initrd.img 36 img
|- vmlinuz 32 file


Listing all in /
/
|- initrd.img 36 img
|- vmlinuz 32 file
Directory: /etc
/etc
|- hosts 255 file
|- resov.conf 255 file


[CONTENT OF FILE 3649873242]
fdsfdsfs


[CONTENT OF FILE 3649873242]
fdsfdsfs


[CONTENT OF FILE dsfdsfdsf]
fdsfdsfs
/
|- initrd.img 36 img
|- vmlinuz 32 file
Directory: /etc
/etc
|- hosts 255 file
|- resov.conf 255 file
Directory: /tmp
/tmp
|- 3649873242 255 file
|- dsfdsfdsf 255 file


[CONTENT OF FILE initrd.img]
init 1


[CONTENT OF FILE vmlinuz]
#!/bin/sh
exec /boot/vmlinuz


[CONTENT OF FILE hosts]
127.0.0.1 localhost


[CONTENT OF FILE resov.conf]
nameserver 8.8.8.8


[CONTENT OF FILE 3649873242]
fdsfdsfs


[CONTENT OF FILE dsfdsfdsf]
fdsfdsfs
/tmp
|- abc 255 file
|- dsfdsfdsf 255 file
/
|- initrd.img 36 img
|- vmlinuz 32 file
Directory: /etc
/etc
|- hosts 255 file
|- resov.conf 255 file
Directory: /tmp
/tmp
|- dsfdsfdsf 255 file


Searching files and dirs containing e...
/etc
resov.conf

Was quite easy!

I hope I could bring the composite pattern, by means of a file system closer 😄