initial
This commit is contained in:
commit
852762ddfc
0
README.rst
Normal file
0
README.rst
Normal file
0
bunker/__init__.py
Normal file
0
bunker/__init__.py
Normal file
0
bunker/backends/__init__.py
Normal file
0
bunker/backends/__init__.py
Normal file
0
bunker/files/__init__.py
Normal file
0
bunker/files/__init__.py
Normal file
146
bunker/files/bunkerfile.py
Normal file
146
bunker/files/bunkerfile.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import tarfile
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
class BunkeredFile(io.RawIOBase):
|
||||||
|
def __init__(self
|
||||||
|
, file_
|
||||||
|
, name
|
||||||
|
, size=None
|
||||||
|
, isvirtual=False
|
||||||
|
, ismem=False
|
||||||
|
, bunker=None):
|
||||||
|
self._file = file_
|
||||||
|
self.isvirtual = isvirtual
|
||||||
|
self.ismem = ismem
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.size = size
|
||||||
|
self.bunker = bunker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Those are just the methods inherited from RawIOBase.
|
||||||
|
# We need them to operate like a proper file.
|
||||||
|
def read(self, size=-1):
|
||||||
|
return self._file.read(size)
|
||||||
|
def readinto(self, b):
|
||||||
|
return self._file.readinto(b)
|
||||||
|
def readall(self):
|
||||||
|
return self._file.readall()
|
||||||
|
def write(self, b):
|
||||||
|
return self._file.write(b)
|
||||||
|
def close(self, writeback=True):
|
||||||
|
if(writeback and self.bunker):
|
||||||
|
self.bunker.writeback_file(self.name)
|
||||||
|
return self._file.close()
|
||||||
|
def fileno(self):
|
||||||
|
return self._file.fileno()
|
||||||
|
def flush(self):
|
||||||
|
return self._file.flush()
|
||||||
|
def isatty(self):
|
||||||
|
return self._file.isatty()
|
||||||
|
def readable(self):
|
||||||
|
return self._file.readable()
|
||||||
|
def readline(self, size=-1):
|
||||||
|
return self._file.readline(size)
|
||||||
|
def readlines(self, hint=-1):
|
||||||
|
return self._file.readlines(hint)
|
||||||
|
def seek(self, offset, whence=0):
|
||||||
|
return self._file.seek(offset, whence)
|
||||||
|
def seekable(self):
|
||||||
|
return self._file.seekable()
|
||||||
|
def tell(self):
|
||||||
|
return self._file.tell()
|
||||||
|
def truncate(self, size=None):
|
||||||
|
return self._file.truncate(size)
|
||||||
|
def writable(self):
|
||||||
|
return self._file.writable()
|
||||||
|
def writelines(self, lines):
|
||||||
|
return self._file.writelines(lines)
|
||||||
|
def __del__(self):
|
||||||
|
del(self._file)
|
||||||
|
@property
|
||||||
|
def closed(self):
|
||||||
|
return self._file.closed
|
||||||
|
|
||||||
|
|
||||||
|
# Classmethods to construct new BunkeredFiles.
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, file_, name):
|
||||||
|
"""
|
||||||
|
Construct a new BunkeredFile from an existing file on disk.
|
||||||
|
This is used to bunker files and directories from the disk.
|
||||||
|
|
||||||
|
DO NOT (!) use this to construct BunkeredFiles from BytesIO.
|
||||||
|
"""
|
||||||
|
size = None
|
||||||
|
if(file_.seekable()):
|
||||||
|
size = file_.seek(0, 2)
|
||||||
|
file_.seek(0, 0)
|
||||||
|
return cls(file_, name, size=size, isvirtual=False, ismem=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def empty(cls, name):
|
||||||
|
"""
|
||||||
|
Construct a new BunkeredFile that uses BytesIO in the background.
|
||||||
|
|
||||||
|
This is used either when loading data from a remote ressource or for
|
||||||
|
databases.
|
||||||
|
"""
|
||||||
|
file_ = io.BytesIO()
|
||||||
|
return cls(file_, name, size=None, isvirtual=True, ismem=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_BytesIO(cls, bytes_, name):
|
||||||
|
size = None
|
||||||
|
if(file_.seekable()):
|
||||||
|
size = file_.seek(0, 2)
|
||||||
|
file_.seek(0, 0)
|
||||||
|
return cls(bytes_, name, size=size, isvirtual=True, ismem=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tar(cls
|
||||||
|
, tarfile
|
||||||
|
, tarinfo
|
||||||
|
, rewriteable_tar_file=None
|
||||||
|
, max_in_memory_bytes=2**20
|
||||||
|
, mktempfile=tempfile.TemporaryFile):
|
||||||
|
"""
|
||||||
|
Load the file specified by ``tarinfo`` from ``tarfile``. If the size of the file
|
||||||
|
is smaller than ``max_in_memory_bytes`` it will be loaded into memory which increases speed
|
||||||
|
security because an attacker cannot find the file on-disk.
|
||||||
|
|
||||||
|
If the size is greated than ``max_in_memory_bytes`` it will be loaded into a file created by
|
||||||
|
``mstemp``. In order to increase security this function should create a file that is hard to find.
|
||||||
|
Having the file on the disk is a security vulnerability, because while the file is open it can be found.
|
||||||
|
|
||||||
|
Also this method is vulnerable to malformed tar files.
|
||||||
|
See `the docs of tarfile <https://docs.python.org/3.5/library/tarfile.html#tarfile.TarFile.extractall>`_.
|
||||||
|
"""
|
||||||
|
if(tarinfo.size > max_in_memory_bytes):
|
||||||
|
file_ = mktempfile()
|
||||||
|
ismem = False
|
||||||
|
else:
|
||||||
|
file_ = io.BytesIO()
|
||||||
|
ismem = True
|
||||||
|
|
||||||
|
with tarfile.extractfile(tarinfo) as fin:
|
||||||
|
print(file_.write(fin.read()))
|
||||||
|
file_.seek(0, 0)
|
||||||
|
return cls(file_, tarinfo.name, size=tarinfo.size, ismem=ismem, isvirtual=True, bunker=rewriteable_tar_file)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
if(not self._file.seekable()):
|
||||||
|
return 0
|
||||||
|
rewind = self._file.tell()
|
||||||
|
self._file.seek(0, 2)
|
||||||
|
length = self._file.tell()
|
||||||
|
self._file.seek(rewind, 0)
|
||||||
|
return length
|
||||||
|
|
||||||
|
def rewind(self):
|
||||||
|
self._file.seek(0, 0)
|
||||||
|
|
100
bunker/files/tarfile.py
Normal file
100
bunker/files/tarfile.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from .bunkerfile import BunkeredFile
|
||||||
|
|
||||||
|
class RewriteableTarFile(object):
|
||||||
|
def __init__(self, path):
|
||||||
|
self._path = path
|
||||||
|
if(not os.path.exists(path) or not tarfile.is_tarfile(path)):
|
||||||
|
raise OSError("file {} does not exist or is not a tar file".format(path))
|
||||||
|
self._open_files = dict()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, path):
|
||||||
|
if(not os.path.exists(path)):
|
||||||
|
tarfile.open(name=path, mode="x").close()
|
||||||
|
return cls(path)
|
||||||
|
|
||||||
|
def _open_handle(self, mode="r"):
|
||||||
|
return tarfile.open(name=self._path, mode=mode)
|
||||||
|
|
||||||
|
def get_file(self, membername, max_in_memory_bytes=2**20, mktempfile=tempfile.TemporaryFile):
|
||||||
|
if(membername in self._open_files):
|
||||||
|
return self._open_files[membername]
|
||||||
|
|
||||||
|
handle = self._open_handle()
|
||||||
|
info = handle.getmember(membername)
|
||||||
|
|
||||||
|
file_ = BunkeredFile.from_tar(handle, info, self)
|
||||||
|
self._open_files[membername] = file_
|
||||||
|
return file_
|
||||||
|
|
||||||
|
def writeback_file(self, membername):
|
||||||
|
if(not membername in self._open_files):
|
||||||
|
raise KeyError("cannot find open file")
|
||||||
|
handle = self._open_handle()
|
||||||
|
os.unlink(self._path)
|
||||||
|
|
||||||
|
open_file = self._open_files[membername]
|
||||||
|
open_file.seek(0, 0)
|
||||||
|
|
||||||
|
new_handle = self._open_handle(mode="x")
|
||||||
|
|
||||||
|
for member in handle.getmembers():
|
||||||
|
if(member.name == membername):
|
||||||
|
member.size = len(open_file)
|
||||||
|
open_file.rewind()
|
||||||
|
new_handle.addfile(member, open_file)
|
||||||
|
continue
|
||||||
|
new_handle.addfile(tarinfo=member, fileobj=handle.extractfile(member))
|
||||||
|
new_handle.close()
|
||||||
|
del(self._open_files[membername])
|
||||||
|
|
||||||
|
def add_file(self, file_: BunkeredFile):
|
||||||
|
handle = self._open_handle(mode="a")
|
||||||
|
tarinfo = tarfile.TarInfo(name=file_.name)
|
||||||
|
tarinfo.size = len(file_)
|
||||||
|
file_.rewind()
|
||||||
|
handle.addfile(tarinfo=tarinfo
|
||||||
|
, fileobj=file_)
|
||||||
|
handle.close()
|
||||||
|
|
||||||
|
def delete_file(self, membername):
|
||||||
|
if(membername in self._open_files):
|
||||||
|
del(self._open_files[membername])
|
||||||
|
|
||||||
|
handle = self._open_handle()
|
||||||
|
os.unlink(self._path)
|
||||||
|
|
||||||
|
new_handle = self._open_handle(mode="x")
|
||||||
|
|
||||||
|
for member in handle.getmembers():
|
||||||
|
if(member.name == membername):
|
||||||
|
continue
|
||||||
|
new_handle.add_file(member, handle.extractfile(member))
|
||||||
|
new_handle.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Writes back all open files.
|
||||||
|
"""
|
||||||
|
if(not self._open_files):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write back all open files.
|
||||||
|
handle = self._open_handle()
|
||||||
|
os.unlink(self._path)
|
||||||
|
new_handle = self._open_handle(mode="x")
|
||||||
|
|
||||||
|
for member in handle.getmembers():
|
||||||
|
if(member.name in self._open_files):
|
||||||
|
member.size = len(self._open_files[member.name])
|
||||||
|
self._open_files[member.name].rewind()
|
||||||
|
new_handle.addfile(tarinfo=member, fileobj=self._open_files[member.name])
|
||||||
|
self._open_files[member.name].close(writeback=False)
|
||||||
|
continue
|
||||||
|
new_handle.addfile(member, handle.extractfile(member))
|
||||||
|
new_handle.close()
|
||||||
|
|
35
setup.py
Normal file
35
setup.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Copyright (c) 2018 Daniel Knüttel #
|
||||||
|
# #
|
||||||
|
# This file is part of licor. #
|
||||||
|
# #
|
||||||
|
# licor is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# licor is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU Affero General Public License #
|
||||||
|
# along with licor. If not, see <http://www.gnu.org/licenses/>. #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = "bunker",
|
||||||
|
version = "0.0.0",
|
||||||
|
packages = find_packages(),
|
||||||
|
author = "Daniel Knüttel",
|
||||||
|
author_email = "daniel.knuettel@daknuett.eu",
|
||||||
|
url = "https://daknuett.eu/gitea/daknuett/bunker",
|
||||||
|
#install_requires = ["docopt"],
|
||||||
|
description = "A module for encrypted data storage",
|
||||||
|
long_description = open("README.rst").read(),
|
||||||
|
|
||||||
|
#entry_points = {"console_scripts": ["licor = licor.main:main"]}
|
||||||
|
)
|
||||||
|
|
29
test/test_files_bunkerfile.py
Normal file
29
test/test_files_bunkerfile.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from bunker.files.bunkerfile import BunkeredFile
|
||||||
|
|
||||||
|
def test_load_from_tar(tmpdir):
|
||||||
|
tmpdname = str(tmpdir)
|
||||||
|
|
||||||
|
with open(os.path.join(tmpdname, "a.tx"), "wb") as f:
|
||||||
|
f.write(b"abcdefg")
|
||||||
|
with open(os.path.join(tmpdname, "b.tx"), "wb") as f:
|
||||||
|
f.write(b"foobar")
|
||||||
|
|
||||||
|
f = tarfile.TarFile(os.path.join(tmpdname, "test.tar"), "w")
|
||||||
|
|
||||||
|
f.add(os.path.join(tmpdname, "a.tx"))
|
||||||
|
f.add(os.path.join(tmpdname, "b.tx"))
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
f = tarfile.TarFile(os.path.join(tmpdname, "test.tar"))
|
||||||
|
|
||||||
|
ainfo = f.next()
|
||||||
|
binfo = f.next()
|
||||||
|
|
||||||
|
a = BunkeredFile.from_tar(f, ainfo)
|
||||||
|
b = BunkeredFile.from_tar(f, binfo)
|
||||||
|
|
||||||
|
assert a.read() == b"abcdefg"
|
||||||
|
assert b.read() == b"foobar"
|
23
test/test_files_tarfile.py
Normal file
23
test/test_files_tarfile.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from bunker.files.tarfile import RewriteableTarFile
|
||||||
|
from bunker.files.bunkerfile import BunkeredFile
|
||||||
|
|
||||||
|
|
||||||
|
def test_create(tmpdir):
|
||||||
|
tmpdname = str(tmpdir)
|
||||||
|
|
||||||
|
tf = RewriteableTarFile.open(os.path.join(tmpdname, "test.bunker"))
|
||||||
|
f = BunkeredFile.empty("__bunker_main__")
|
||||||
|
tf.add_file(f)
|
||||||
|
|
||||||
|
f = tf.get_file("__bunker_main__")
|
||||||
|
f.write(b"foobar")
|
||||||
|
|
||||||
|
tf.close()
|
||||||
|
|
||||||
|
tf = RewriteableTarFile.open(os.path.join(tmpdname, "test.bunker"))
|
||||||
|
f = tf.get_file("__bunker_main__")
|
||||||
|
|
||||||
|
assert f.read() == b"foobar"
|
Loading…
Reference in New Issue
Block a user