mirror of https://github.com/CIRCL/Circlean
Initial and partial reimplementation in Python.
parent
6ae56e26cb
commit
0bbc717878
|
@ -0,0 +1,273 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import magic
|
||||
import os
|
||||
import shutil
|
||||
import mimetypes
|
||||
from twiggy import quickSetup, log
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
LIBREOFFICE = '/usr/bin/libreoffice'
|
||||
PDF2HTMLEX = '/usr/bin/pdf2htmlEX'
|
||||
SEVENZ = '/usr/bin/7z'
|
||||
|
||||
|
||||
# Prepare application/<subtype>
|
||||
mimes_office = ['msword', 'vnd.openxmlformats-officedocument.', 'vnd.ms-',
|
||||
'vnd.oasis.opendocument']
|
||||
mimes_pdf = ['pdf']
|
||||
mimes_xml = ['xml']
|
||||
mimes_ms = ['x-dosexec']
|
||||
mimes_compressed = ['zip', 'x-rar', 'x-bzip2', 'x-lzip', 'x-lzma', 'x-lzop',
|
||||
'x-xz', 'x-compress', 'x-gzip', 'x-tar', 'compressed']
|
||||
mimes_unknown = ['octet-stream']
|
||||
|
||||
|
||||
class KittenGroomer(object):
|
||||
|
||||
def __init__(self, max_recursive=5):
|
||||
self.src_root_dir = os.path.join(os.sep, 'media', 'src')
|
||||
self.dst_root_dir = os.path.join(os.sep, 'media', 'dst')
|
||||
self.log_root_dir = os.path.join(self.dst_root_dir, 'logs')
|
||||
self.log_processing = os.path.join(self.log_root_dir, 'processing.log')
|
||||
|
||||
self.recursive = 0
|
||||
self.max_recursive = max_recursive
|
||||
|
||||
# quickSetup(file=self.log_processing)
|
||||
quickSetup()
|
||||
self.log_name = None
|
||||
|
||||
self.cur_src = None
|
||||
self.cur_dst = None
|
||||
self.cur_type = None
|
||||
self.cur_subtype = None
|
||||
|
||||
subtypes_apps = [
|
||||
(mimes_office, self._office_related),
|
||||
(mimes_pdf, self._pdf),
|
||||
(mimes_xml, self._office_related),
|
||||
(mimes_ms, self.dangerous),
|
||||
(mimes_compressed, self.archive),
|
||||
(mimes_unknown, self.unknown),
|
||||
]
|
||||
self.subtypes_application = self._init_subtypes_application(subtypes_apps)
|
||||
|
||||
self.mime_processing_options = {
|
||||
'text': self.text,
|
||||
'audio': self.audio,
|
||||
'image': self.image,
|
||||
'video': self.video,
|
||||
'application': self.application,
|
||||
'example': self.example,
|
||||
'message': self.message,
|
||||
'model': self.model,
|
||||
'multipart': self.multipart,
|
||||
'inode': self.inode,
|
||||
}
|
||||
|
||||
def _init_subtypes_application(self, subtypes_application):
|
||||
to_return = {}
|
||||
for list_subtypes, fct in subtypes_application:
|
||||
for st in list_subtypes:
|
||||
to_return[st] = fct
|
||||
return to_return
|
||||
|
||||
# ##### Discarded mime types, reason in the comments ######
|
||||
def inode(self):
|
||||
''' Usually empty file. No reason (?) to copy it on the dest key'''
|
||||
pass
|
||||
|
||||
def unknown(self):
|
||||
''' This main type is inknown, that should not happen '''
|
||||
pass
|
||||
|
||||
#######################
|
||||
|
||||
def check_mime(self):
|
||||
# /usr/share/mime has interesting stuff
|
||||
|
||||
# guess_type uses the extension to get a mime type
|
||||
expected_mimetype, encoding = mimetypes.guess_type(self.cur_src, strict=False)
|
||||
if expected_mimetype is not None:
|
||||
expected_extensions = mimetypes.guess_all_extensions(expected_mimetype,
|
||||
strict=False)
|
||||
else:
|
||||
# the extension is unknown...
|
||||
expected_extensions = None
|
||||
|
||||
actual_mimetype = '{}/{}'.format(self.cur_type, self.cur_subtype)
|
||||
path, actual_extension = os.path.splitext(self.cur_src)
|
||||
|
||||
if expected_extensions is None and expected_mimetype is None:
|
||||
# Unknown ext/mime, that happens
|
||||
return (None, actual_mimetype, actual_extension)
|
||||
|
||||
matching_mimetype = actual_mimetype == expected_mimetype
|
||||
matching_extension = actual_extension in expected_extensions
|
||||
|
||||
details = {}
|
||||
if not matching_mimetype:
|
||||
details['mimes'] = expected_mimetype
|
||||
if not matching_extension:
|
||||
details['extensions'] = expected_extensions
|
||||
|
||||
if len(details) == 0:
|
||||
# Everything is fine
|
||||
return (True, actual_mimetype, actual_extension)
|
||||
return (False, actual_mimetype, actual_extension, details)
|
||||
|
||||
def _safe_rmtree(self, directory):
|
||||
if os.path.exists(directory):
|
||||
shutil.rmtree(directory)
|
||||
|
||||
def _safe_remove(self, filepath):
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
def _safe_mkdir(self, directory):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
def _safe_copy(self):
|
||||
''' Create dir if needed '''
|
||||
try:
|
||||
dst_path, filename = os.path.split(self.cur_dst)
|
||||
self._safe_mkdir(dst_path)
|
||||
shutil.copy(self.cur_src, self.cur_dst)
|
||||
return True
|
||||
except Exception as e:
|
||||
# TODO: Logfile
|
||||
print(e)
|
||||
return False
|
||||
|
||||
def copy(self):
|
||||
return self._safe_copy()
|
||||
|
||||
def dangerous(self):
|
||||
path, filename = os.path.split(self.cur_dst)
|
||||
self.cur_dst = os.path.join(path, 'DANGEROUS_{}_DANGEROUS'.format(filename))
|
||||
return self._safe_copy()
|
||||
|
||||
def run_process(self, command_line):
|
||||
args = shlex.split(command_line)
|
||||
p = subprocess.Popen(args)
|
||||
while True:
|
||||
code = p.poll()
|
||||
if code is not None:
|
||||
break
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
||||
# ##### Converted ######
|
||||
def _office_related(self):
|
||||
dst_path, filename = os.path.split(self.cur_dst)
|
||||
tmpdir = os.path.join(dst_path, 'temp')
|
||||
self._safe_mkdir(tmpdir)
|
||||
# TODO; Convert to pdf/A
|
||||
lo_command = '{} --headless --convert-to pdf --outdir {} {}'.format(
|
||||
LIBREOFFICE, tmpdir, self.cur_src)
|
||||
self.run_process(lo_command)
|
||||
pdf_command = '{} --dest-dir=/ {}/*.pdf {}'.format(PDF2HTMLEX, tmpdir, self.cur_dst)
|
||||
self.run_process(pdf_command)
|
||||
self._safe_rmtree(tmpdir)
|
||||
|
||||
def _pdf(self):
|
||||
# TODO: Convert to pdf/A
|
||||
pdf_command = '{} --dest-dir {} {}'.format(PDF2HTMLEX, self.cur_dst, self.cur_src)
|
||||
self.run_process(pdf_command)
|
||||
|
||||
def archive(self):
|
||||
self.recursive += 1
|
||||
tmpdir = self.cur_dst + '_temp'
|
||||
self._safe_mkdir(tmpdir)
|
||||
extract_command = '{} -p1 x {} -o{} -bd'.format(SEVENZ, self.cur_src, tmpdir)
|
||||
self.run_process(extract_command)
|
||||
self.processdir(self.cur_dst, tmpdir)
|
||||
self._safe_rmtree(tmpdir)
|
||||
self.recursive -= 1
|
||||
|
||||
def text(self):
|
||||
''' LibreOffice should be able to open all the files '''
|
||||
return self._office_related()
|
||||
|
||||
def application(self):
|
||||
''' Everything can be there, using the subtype to decide '''
|
||||
return self.subtypes_application.get(self.cur_subtype, self.unknown)()
|
||||
|
||||
#######################
|
||||
|
||||
# ##### Not converted, checking the mime type ######
|
||||
def _media_processing(self):
|
||||
clean, mimetype, extension, details = self.check_mime()
|
||||
# TODO: write details in the logfile
|
||||
|
||||
if clean:
|
||||
# Just copy, the extension matches the mime type
|
||||
return self.copy()
|
||||
else:
|
||||
# The extension is unknown or doesn't match the mime type, suspicious
|
||||
return self.dangerous()
|
||||
|
||||
def audio(self):
|
||||
return self._media_processing()
|
||||
|
||||
def image(self):
|
||||
return self._media_processing()
|
||||
|
||||
def video(self):
|
||||
return self._media_processing()
|
||||
|
||||
#######################
|
||||
|
||||
# ##### Threated as malicious, no reason to have it on a USB key ######
|
||||
def example(self):
|
||||
return self.dangerous()
|
||||
|
||||
def message(self):
|
||||
return self.dangerous()
|
||||
|
||||
def model(self):
|
||||
return self.dangerous()
|
||||
|
||||
def multipart(self):
|
||||
return self.dangerous()
|
||||
|
||||
#######################
|
||||
|
||||
def _list_all_files(self, directory):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for filename in files:
|
||||
filepath = os.path.join(root, filename)
|
||||
mimetype = magic.from_file(filepath, mime=True)
|
||||
maintype, subtype = mimetype.split('/')
|
||||
yield filepath, maintype, subtype
|
||||
|
||||
def processdir(self, dst_dir=None, src_dir=None):
|
||||
if dst_dir is None:
|
||||
dst_dir = self.dst_root_dir
|
||||
if src_dir is None:
|
||||
src_dir = self.src_root_dir
|
||||
if self.recursive >= self.max_recursive:
|
||||
self._safe_rmtree(src_dir)
|
||||
if src_dir.endswith('_temp'):
|
||||
archbomb_path = src_dir[:-len('_temp')]
|
||||
self._safe_remove(archbomb_path)
|
||||
|
||||
logname_files = log.name('files')
|
||||
for srcpath, maintype, subtype in self._list_all_files(src_dir):
|
||||
logname_files.info('Processing {} ({}/{})', srcpath, maintype, subtype)
|
||||
self.cur_src = srcpath
|
||||
self.cur_dst = srcpath.replace(src_dir, dst_dir)
|
||||
self.cur_type = maintype
|
||||
self.cur_subtype = subtype
|
||||
|
||||
self.mime_processing_options.get(maintype, self.unknown)()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
kg = KittenGroomer()
|
||||
kg.processdir('dst', 'src')
|
Loading…
Reference in New Issue