Commit 8ec7e1c1 authored by Laurent Bachelier's avatar Laurent Bachelier

Add an extensible cleanup command

There are two phases: checking, then optionally removing (details in
ICleaner).
Plugins can register their own cleaners thanks to the hook mechanism.
Added a simple cleaner in the Core plugin.
parent 8ad6b30a
......@@ -182,3 +182,19 @@ class File(IObject):
self.data['info'].pop('view', None)
self.data['info']['path'] = self.path
self.data['perms'] = self.perms
class UnknownFile(File):
"""
File not know by its path, but only by its hash.
This should only be used for special cases.
"""
def __init__(self, storage, hsh):
self._confname = os.path.join('files', hsh)
File.__init__(self, storage, '')
self.read()
self.path = self.data['info'].get('path')
def _get_confname(self):
return self._confname
# -*- coding: utf-8 -*-
# Copyright (C) 2011 Romain Bignon, Laurent Bachelier
#
# This file is part of ass2m.
#
# ass2m 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.
#
# ass2m 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with ass2m. If not, see <http://www.gnu.org/licenses/>.
from ass2m.plugin import Plugin
from ass2m.cmd import Command
__all__ = ['CleanupPlugin']
class ICleaner(object):
def __init__(self, storage):
self.storage = storage
def fsck(self):
"""
Checks integrity, perform uprades.
This is the required first part of a cleanup.
It should avoid destroying information.
"""
raise NotImplementedError()
def gc(self):
"""
Remove useless, outdated, invalid, etc. entries. This is optional.
"""
raise NotImplementedError()
def remove(self, obj):
"""
Remove a storage object.
Checks if it still exists before attempting, since
it could have been deleted for another reason in the
same cleanup run.
"""
obj.read()
if obj.exists:
obj.remove()
print "Removed %s." % obj._get_confname()
class CleanupCmd(Command):
DESCRIPTION = 'Check and fix the ass2m storage data'
@staticmethod
def configure_parser(parser):
parser.add_argument('-g', '--gc', help='Run garbage collection', \
action='store_true', dest='gc')
def cmd(self, args):
cleaners = [cleaner(self.storage) \
for cleaner in self.butt.hooks['cleanup']]
for cleaner in cleaners:
cleaner.fsck()
if args.gc:
for cleaner in cleaners:
cleaner.gc()
class CleanupPlugin(Plugin):
def init(self):
self.register_cli_command('cleanup', CleanupCmd)
......@@ -28,6 +28,7 @@ from ass2m.files import File
from ass2m.routes import View
from ass2m.server import ViewAction, FileApp
from .cleanup import ICleaner
__all__ = ['CorePlugin']
......@@ -189,6 +190,19 @@ class ListAction(ViewAction):
self.ctx.res.body = self.ctx.render('list.html')
class CoreCleaner(ICleaner):
def fsck(self):
self.invalid_paths = []
for f in self.storage.iter_files():
if f.path is None or f._get_confname() != File._get_confname(f):
print "%s has an invalid path: %s." % (f._get_confname(), f.path)
self.invalid_paths.append(f)
def gc(self):
for f in self.invalid_paths:
self.remove(f)
class CorePlugin(Plugin):
def init(self):
self.register_cli_command('init', InitCmd)
......@@ -202,3 +216,5 @@ class CorePlugin(Plugin):
self.register_web_view(
View(object_type='directory', name='list', verbose_name='Detailed list'),
ListAction, 10)
self.register_hook('cleanup', CoreCleaner)
......@@ -23,7 +23,7 @@ import sys
from ConfigParser import RawConfigParser
from .users import Group, User, Anonymous
from .files import File
from .files import File, UnknownFile
from .obj import IObject, ConfigDict
......@@ -109,6 +109,10 @@ class Storage(object):
return storage
def get_user(self, name):
"""
Get a particular user by its name.
Returns Anonymous if no user is found.
"""
user = User(self, name)
user.read()
if not user.exists:
......@@ -116,14 +120,29 @@ class Storage(object):
return user
def iter_users(self):
"""
Get all stored users.
"""
usersdir = os.path.join(self.path, 'users')
if os.path.exists(usersdir):
for name in sorted(os.listdir(usersdir)):
user = self.get_user(name)
if user:
yield user
yield user
def iter_files(self):
"""
Get all files with a stored configuration.
"""
filesdir = os.path.join(self.path, 'files')
if os.path.exists(filesdir):
for hsh in sorted(os.listdir(filesdir)):
f = UnknownFile(self, hsh)
yield f
def user_exists(self, name):
"""
Test if an user exists by its name. Returns boolean.
"""
user = User(self, name)
user.read()
return user.exists
......
from unittest import TestCase
from ass2m.cli import CLI
from ass2m.storage import Storage
from tempfile import mkdtemp
import shutil
import os
import sys
from StringIO import StringIO
class CleanupTest(TestCase):
def setUp(self):
self.root = mkdtemp(prefix='ass2m_test_root')
self.storage = Storage.create(self.root)
self.app = CLI(self.root)
def tearDown(self):
if self.root:
shutil.rmtree(self.root)
def beginCapture(self, with_stderr=False):
self.stdout = sys.stdout
# begin capture
sys.stdout = StringIO()
if with_stderr:
self.stderr = sys.stderr
sys.stderr = sys.stdout
elif not hasattr(self, 'stderr'):
self.stderr = None
def endCapture(self):
captured = sys.stdout.getvalue()
# end capture
if self.stdout is not None:
sys.stdout = self.stdout
self.stdout = None
if self.stderr is not None:
sys.stderr = self.stderr
self.stderr = None
return captured
def test_simpleRun(self):
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup']) in (0, None)
output = self.endCapture()
assert output.strip() == ''
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup', '--gc']) in (0, None)
output = self.endCapture()
assert output.strip() == ''
def test_invalidPaths(self):
with open(os.path.join(self.storage.path, 'files', 'hello'), 'w') as f:
f.write('')
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup']) in (0, None)
output = self.endCapture()
assert output.strip() == 'files/hello has an invalid path: None.'
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup', '--gc']) in (0, None)
output = self.endCapture()
assert output.strip() == 'files/hello has an invalid path: None.\nRemoved files/hello.'
with open(os.path.join(self.storage.path, 'files', 'c93c3312483174a3170ebe7395612c404a0620d0'), 'w') as f:
f.write('[info]\npath = /hello')
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup']) in (0, None)
output = self.endCapture()
assert output.strip() == 'files/c93c3312483174a3170ebe7395612c404a0620d0 has an invalid path: /hello.'
self.beginCapture()
assert self.app.main(['ass2m_test', 'cleanup', '--gc']) in (0, None)
output = self.endCapture()
assert output.strip() == 'files/c93c3312483174a3170ebe7395612c404a0620d0 has an invalid path: /hello.\nRemoved files/c93c3312483174a3170ebe7395612c404a0620d0.'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment