#!/usr/bin/env python # -*- coding: utf-8 -*- # IMPORTANTE: # Esse script precisa rodar em python 2 # e depende apenas do descrito no arquivo requiments.txt import cStringIO import hashlib import mimetypes import os import sys from collections import defaultdict from contextlib import contextmanager from functools import partial from os.path import exists import git import magic import yaml import ZODB.DB import ZODB.FileStorage from unipath import Path from ZODB.broken import Broken from ZODB.POSException import POSKeyError from variaveis_comuns import DIR_DADOS_MIGRACAO, TAG_ZOPE EXTENSOES = { 'application/msword': '.doc', 'application/pdf': '.pdf', 'application/vnd.oasis.opendocument.text': '.odt', 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa 'application/vnd.oasis.opendocument.text-template': '.ott', 'application/vnd.ms-powerpoint': '.ppt', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', # noqa 'application/vnd.oasis.opendocument.spreadsheet': '.ods', 'application/xml': '.xml', 'text/xml': '.xml', 'application/zip': '.zip', 'application/x-rar': '.rar', 'image/jpeg': '.jpeg', 'image/png': '.png', 'image/gif': '.gif', 'text/html': '.html', 'text/rtf': '.rtf', 'text/x-python': '.py', 'text/plain': '.txt', 'SDE-Document': '.xml', 'image/tiff': '.tiff', 'application/tiff': '.tiff', 'audio/x-wav': '.wav', 'video/mp4': '.mp4', 'image/x-icon': '.ico', 'image/x-ms-bmp': '.bmp', 'video/x-ms-asf': '.asf', 'audio/mpeg': '.mp3', # TODO rever... 'text/richtext': '.rtf', # sem extensao 'application/octet-stream': '', # binário 'inode/x-empty': '', # vazio 'application/x-empty': '', # vazio 'text/x-unknown-content-type': '', # desconhecido 'application/CDFV2-unknown': '', # desconhecido } def br(obj): if isinstance(obj, Broken): return obj.__Broken_state__ else: return obj def guess_extension(fullname, buffer): mime = magic.from_buffer(buffer[:1024], mime=True) extensao = EXTENSOES.get(mime) if extensao is not None: return extensao else: possibilidades = '\n'.join( [" '{}': '{}',".format(mime, ext) for ext in mimetypes.guess_all_extensions(mime)]) print('''Extensão não conhecida para o arquivo: {} e mimetype: {} Algumas possibilidades são: {} Atualize o código do dicionário EXTENSOES! '''.format(fullname, mime, possibilidades) ) return '.DESCONHECIDO.{}'.format(mime.replace('/', '__')) CONTEUDO_ARQUIVO_CORROMPIDO = 'ARQUIVO CORROMPIDO' def get_conteudo_file(doc): # A partir daqui usamos dict.pop('...') nos __Broken_state__ # para contornar um "vazamento" de memória que ocorre # ao percorrer a árvore de objetos # # Imaginamos que, internamente, o ZODB está guardando referências # para os objetos Broken criados e não conseguimos identificar como. # # Essa medida descarta quase todos os dados retornados # e só funciona na primeira passagem try: pdata = br(doc.pop('data')) if isinstance(pdata, str): # Retrocedemos se pdata ja eh uma str (necessario em Images) doc['data'] = pdata pdata = doc output = cStringIO.StringIO() while pdata: output.write(pdata.pop('data')) pdata = br(pdata.pop('next', None)) return output.getvalue() except POSKeyError: return CONTEUDO_ARQUIVO_CORROMPIDO def dump_file(doc, path, salvar, get_conteudo=get_conteudo_file): name = doc['__name__'] fullname = os.path.join(path, name) conteudo = get_conteudo(doc) if conteudo == CONTEUDO_ARQUIVO_CORROMPIDO: fullname = fullname + '_CORROMPIDO' print('ATENÇÃO: arquivo corrompido: {}'.format(fullname)) if conteudo: # pula arquivos vazios salvar(fullname, conteudo) return name def get_conteudo_dtml_method(doc): return doc['raw'] def print_msg_poskeyerror(id): print('#' * 80) print('#' * 80) print('ATENÇÃO: DIRETÓRIO corrompido: {}'.format(id)) print('#' * 80) print('#' * 80) def enumerate_by_key_list(folder, key_list, type_key): for entry in folder.get(key_list, []): id, meta_type = entry['id'], entry[type_key] try: obj = folder.get(id, None) except POSKeyError: print_msg_poskeyerror(id) else: yield id, obj, meta_type enumerate_folder = partial(enumerate_by_key_list, key_list='_objects', type_key='meta_type') enumerate_properties = partial(enumerate_by_key_list, key_list='_properties', type_key='type') def enumerate_btree(folder): contagem_esperada = folder['_count'].value tree = folder['_tree'] contagem_real = 0 # para o caso em que não haja itens try: for contagem_real, (id, obj) in enumerate(tree.iteritems(), start=1): meta_type = type(obj).__name__ yield id, obj, meta_type except POSKeyError: print_msg_poskeyerror(folder['id']) # verificação de consistência if contagem_esperada != contagem_real: print('ATENÇÃO: contagens diferentes na btree: ' '{} esperada: {} real: {}'.format(folder['title'], contagem_esperada, contagem_real)) nao_identificados = defaultdict(list) @contextmanager def logando_nao_identificados(): nao_identificados.clear() yield if nao_identificados: print('#' * 80) print('#' * 80) print('FORAM ENCONTRADOS ARQUIVOS DE FORMATO NÃO IDENTIFICADO!!!') print('REFAÇA A EXPORTAÇÃO\n') print(nao_identificados) print('#' * 80) print('#' * 80) def dump_folder(folder, path, salvar, mtimes, enum=enumerate_folder): name = folder['id'] path = os.path.join(path, name) if not exists(path): os.makedirs(path) for id, obj, meta_type in enum(folder): # pula pastas *_old (presentes em várias bases) if id.endswith('_old') and meta_type in ['Folder', 'BTreeFolder2']: continue dump = DUMP_FUNCTIONS.get(meta_type, '?') if dump == '?': nao_identificados[meta_type].append(path + '/' + id) elif dump: if isinstance(dump, partial) and dump.func == dump_folder: dump(br(obj), path, salvar, mtimes) else: # se o objeto for mais recente que o da última exportação mtime = obj._p_mtime fullname = os.path.join(path, id) if mtime > mtimes.get(fullname, 0): id_interno = dump(br(obj), path, salvar) assert id == id_interno mtimes[fullname] = mtime return name def decode_iso8859(obj): return obj.decode('iso8859-1') if isinstance(obj, str) else obj def read_sde(element): def read_properties(): for id, obj, meta_type in enumerate_properties(element): yield id, decode_iso8859(br(obj)) def read_children(): for id, obj, meta_type in enumerate_folder(element): assert meta_type in ['SDE-Document-Element', 'SDE-Template-Element', 'SDE-Template-Link', 'SDE-Template-Attribute', 'Script (Python)', ] if meta_type != 'Script (Python)': # ignoramos os scrips python de eventos dos templates yield {'id': id, 'meta_type': meta_type, 'dados': read_sde(br(obj))} data = dict(read_properties()) children = list(read_children()) if children: data['children'] = children return data def save_as_yaml(path, name, obj, salvar): fullname = os.path.join(path, name) conteudo = yaml.safe_dump(obj, allow_unicode=True) salvar(fullname, conteudo) def dump_sde(strdoc, path, salvar, tipo): id = strdoc['id'] sde = read_sde(strdoc) save_as_yaml(path, '{}.{}.yaml'.format(id, tipo), sde, salvar) return id DUMP_FUNCTIONS = { 'File': dump_file, 'Image': dump_file, 'DTML Method': partial(dump_file, get_conteudo=get_conteudo_dtml_method), 'Folder': partial(dump_folder, enum=enumerate_folder), 'BTreeFolder2': partial(dump_folder, enum=enumerate_btree), 'SDE-Document': partial(dump_sde, tipo='sde.document'), 'StrDoc': partial(dump_sde, tipo='sde.document'), 'SDE-Template': partial(dump_sde, tipo='sde.template'), # explicitamente ignorados 'ZCatalog': None, 'Dumper': None, 'CachingPolicyManager': None, } def get_app(data_fs_path): storage = ZODB.FileStorage.FileStorage(data_fs_path, read_only=True) db = ZODB.DB(storage) connection = db.open() root = connection.root() app = br(root['Application']) def close_db(): db.close() return app, close_db def find_sapl(app): ids_meta_types = [(obj['id'], obj['meta_type']) for obj in app['_objects']] # estar ordenado é muito importante para que a busca dê prioridade # a um id "cm_zzz" antes do id "sapl" for id, meta_type in sorted(ids_meta_types): if id.startswith('cm_') and meta_type == 'Folder': cm_zzz = br(app[id]) return find_sapl(cm_zzz) elif id == 'sapl' and meta_type in ['SAPL', 'Folder']: sapl = br(app['sapl']) return sapl def detectar_encoding(fonte): desc = magic.from_buffer(fonte) for termo, enc in [('ISO-8859', 'latin1'), ('UTF-8', 'utf-8')]: if termo in desc: return enc return None def autodecode(fonte): if isinstance(fonte, str): enc = detectar_encoding(fonte) return fonte.decode(enc) if enc else fonte else: return fonte def dump_propriedades(docs, path, salvar): props_sapl = br(docs['props_sapl']) ids = [p['id'] for p in props_sapl['_properties']] props = {id: props_sapl[id] for id in ids} props = {id: autodecode(p) for id, p in props.items()} save_as_yaml(path, 'sapl_documentos/propriedades.yaml', props, salvar) def dump_usuarios(sapl, path, salvar): users = br(br(sapl['acl_users'])['data']) users = {autodecode(k): br(v) for k, v in users['data'].items()} for dados in users.values(): dados['name'] = autodecode(dados['name']) save_as_yaml(path, 'usuarios.yaml', users, salvar) def _dump_sapl(data_fs_path, documentos_fs_path, destino, salvar, mtimes): assert exists(data_fs_path) assert exists(documentos_fs_path) # precisamos trabalhar com strings e não Path's para as comparações de mtimes data_fs_path, documentos_fs_path, destino = map(str, ( data_fs_path, documentos_fs_path, destino)) app, close_db = get_app(data_fs_path) try: sapl = find_sapl(app) # extrai usuários com suas senhas e perfis dump_usuarios(sapl, destino, salvar) finally: close_db() app, close_db = get_app(documentos_fs_path) try: sapl = find_sapl(app) # extrai folhas XSLT if 'XSLT' in sapl: dump_folder(br(sapl['XSLT']), destino, salvar, mtimes) # extrai documentos docs = br(sapl['sapl_documentos']) with logando_nao_identificados(): dump_folder(docs, destino, salvar, mtimes) dump_propriedades(docs, destino, salvar) finally: close_db() def repo_execute(repo, cmd, *args): return repo.git.execute(cmd.split() + list(args)) def ajusta_extensao(fullname, conteudo): base, extensao = os.path.splitext(fullname) if extensao not in ['.xsl', '.xslt', '.yaml', '.css']: extensao = guess_extension(fullname, conteudo) return base + extensao def build_salvar(repo): def salvar(fullname, conteudo): fullname = ajusta_extensao(fullname, conteudo) if exists(fullname): # destrava arquivo pré-existente (o conteúdo mudou) repo_execute(repo, 'git annex unlock', fullname) with open(fullname, 'w') as arq: arq.write(conteudo) print(fullname) return salvar def dump_sapl(sigla): sigla = sigla[-3:] # ignora prefixo (por ex. 'sapl_cm_') data_fs_path, documentos_fs_path = [ DIR_DADOS_MIGRACAO.child( 'datafs', '{}_cm_{}.fs'.format(prefixo, sigla)) for prefixo in ('Data', 'DocumentosSapl')] assert exists(data_fs_path), 'Origem não existe: {}'.format(data_fs_path) if not exists(documentos_fs_path): documentos_fs_path = data_fs_path nome_banco_legado = 'sapl_cm_{}'.format(sigla) destino = DIR_DADOS_MIGRACAO.child('repos', nome_banco_legado) destino.mkdir(parents=True) repo = git.Repo.init(destino) if TAG_ZOPE in repo.tags: print('{}: A exportação de documentos já está feita -- abortando'.format(sigla)) return repo_execute(repo, 'git annex init') repo_execute(repo, 'git config annex.thin true') salvar = build_salvar(repo) try: finalizado = False arq_mtimes = Path(repo.working_dir, 'mtimes.yaml') mtimes = yaml.load( arq_mtimes.read_file()) if arq_mtimes.exists() else {} _dump_sapl(data_fs_path, documentos_fs_path, destino, salvar, mtimes) finalizado = True finally: # grava mundaças repo_execute(repo, 'git annex add sapl_documentos') arq_mtimes.write_file(yaml.safe_dump(mtimes, allow_unicode=True)) repo.git.add(A=True) # atualiza repo if 'master' not in repo.heads or repo.index.diff('HEAD'): # se de fato existe mudança status = 'completa' if finalizado else 'parcial' repo.index.commit(u'Exportação do zope {}'.format(status)) if finalizado: repo.git.execute('git tag -f'.split() + [TAG_ZOPE]) if __name__ == "__main__": if len(sys.argv) == 2: sigla = sys.argv[1] dump_sapl(sigla) else: print('Uso: python exporta_zope ')