Sistema de Apoio ao Processo Legislativo
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

459 lines
14 KiB

#!/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 <sigla>')