diff --git a/.gitignore b/.gitignore
index e65f3d5..144db77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/log/
/earthworm.kdev4
__pycache__
+/repo/
diff --git a/action/actions.py b/action/actions.py
index 3094761..6753c36 100644
--- a/action/actions.py
+++ b/action/actions.py
@@ -1,9 +1,11 @@
from util import mergedict
-import action.user as user
+import action.user
+import action.repo
actions = dict()
-mergedict(actions, user.actions, 'user.')
+mergedict(actions, action.user.actions, 'user.')
+mergedict(actions, action.repo.actions, 'repo.')
diff --git a/action/repo.py b/action/repo.py
new file mode 100644
index 0000000..5b8ea4e
--- /dev/null
+++ b/action/repo.py
@@ -0,0 +1,81 @@
+
+
+import exception
+from action.action import Action
+from action.user import UserBase
+
+
+class RepoBase(UserBase):
+ def parse_repository_id(self, request):
+ repository_id = 0
+ try:
+ repository_id = int(request.postvars.get('repository_id', 0))
+ except Exception:
+ raise exception.ActionError( request.t('Repository Id incorrect') )
+ if not repository_id:
+ raise exception.ActionError( request.t('Repository Id incorrect') )
+ return repository_id
+
+
+class RepoCreate(RepoBase):
+ def process(self, request):
+ user_id = self.parse_user_id(request)
+ name = str(request.postvars.get('name', ''))
+ repotype = str(request.postvars.get('type', ''))
+ title = str(request.postvars.get('title', ''))
+ description = str(request.postvars.get('description', ''))
+
+ repo = None
+ try:
+ repo = request.model.repositories.create(user_id, name, repotype, title, description)
+ except Exception as e:
+ self.propagate_exception(e)
+
+ request.connection.commit()
+ return request.answer.complete_redirect(['user', str(repo.get_user().login), 'repo', str(repo.name)])
+
+
+class RepoUpdate(RepoBase):
+ def process(self, request):
+ repository_id = self.parse_repository_id(request)
+ title = str(request.postvars.get('title', ''))
+ description = str(request.postvars.get('description', ''))
+
+ repo = request.model.repositories.get_by_id(repository_id)
+ if not repo:
+ raise exception.ActionError( request.t('Repository not found') )
+
+ try:
+ repo.update(title, description)
+ except Exception as e:
+ self.propagate_exception(e)
+
+ request.connection.commit()
+ return request.answer.complete_redirect(['user', str(repo.get_user().login), 'repo', str(repo.name)])
+
+
+class RepoDelete(RepoBase):
+ def process(self, request):
+ repository_id = self.parse_repository_id(request)
+
+ repo = request.model.repositories.get_by_id(repository_id)
+ if not repo:
+ raise exception.ActionError( request.t('Repository not found') )
+ login = repo.get_user().login
+
+ try:
+ repo.delete()
+ except Exception as e:
+ self.propagate_exception(e)
+
+ request.connection.commit()
+ return request.answer.complete_redirect(['user', login, 'repos'])
+
+
+actions = {
+ 'create' : RepoCreate(),
+ 'update' : RepoUpdate(),
+ 'delete' : RepoDelete(),
+}
+
+
diff --git a/answer.py b/answer.py
index 70f6b5c..82789ac 100644
--- a/answer.py
+++ b/answer.py
@@ -10,7 +10,6 @@ class Result:
def __init__(self, code, headers, data):
assert(type(code) is str)
assert(type(headers) is list)
- assert(type(data) is list)
self.code = code
self.headers = headers
self.data = data
@@ -45,9 +44,9 @@ class Answer(Translator):
self.objects = dict()
def htmlescape(self, text):
- return html.escape(str(text))
+ return html.escape(str('' if text is None else text))
def urlescape(self, text):
- return urllib.parse.quote(str(text))
+ return urllib.parse.quote(str('' if text is None else text))
def e(self, text):
return self.htmlescape(text)
@@ -68,7 +67,7 @@ class Answer(Translator):
def complete_redirect(self, path = None):
self.status = '303 See Other'
- self.headers.append(('Location', self.request.get_urlpath_escaped(path)))
+ self.headers.append( ('Location', self.request.get_urlpath_escaped(path)) )
return Result(self.status, self.headers, list())
def complete_error(self, status = None):
@@ -102,7 +101,7 @@ class Answer(Translator):
def complete_html(self, html = None):
if not html is None:
self.html = html
- self.headers += [('Content-Type','text/html')];
+ self.headers.append( ('Content-Type','text/html') );
return self.complete_text( self.html )
def complete_content(self, content = None, template = None):
diff --git a/config.py b/config.py
index cb8363b..926930d 100644
--- a/config.py
+++ b/config.py
@@ -1,5 +1,7 @@
config = {
+ 'domain' : 'earthworm.local',
+
'db': {
'prefix' : 'ew_',
'connection': {
@@ -16,5 +18,12 @@ config = {
'selfcreate' : True,
'selfdelete' : True,
},
+
+ 'repositories': {
+ 'git': {
+ 'internalurl' : 'http://localhost:8011/repo/git',
+ 'path' : '/home/bw/work/dev/earthworm/repo/git'
+ },
+ },
}
diff --git a/db/cache.py b/db/cache.py
index 479754c..96072cd 100644
--- a/db/cache.py
+++ b/db/cache.py
@@ -115,24 +115,31 @@ class Cache:
assert(type(table) is str)
sql = self.build_select(connection, table, fields)
- with self.lock:
- tbl = self.tables.get(table)
- if not tbl:
- self.tables[table] = tbl = CacheTable(self.maxcount)
- rows = tbl.get(sql)
+ rows = None
+
+ # cache does not support transactions and disabled
+ #
+ #with self.lock:
+ # tbl = self.tables.get(table)
+ # if not tbl:
+ # self.tables[table] = tbl = CacheTable(self.maxcount)
+ # rows = tbl.get(sql)
if rows is None:
rows = connection.query_dict(sql)
assert(type(rows) is list)
- with self.lock:
- tbl = self.tables.get(table)
- if not tbl:
- self.tables[table] = tbl = CacheTable(self.maxcount)
- tbl.set(sql, rows)
+ #with self.lock:
+ # tbl = self.tables.get(table)
+ # if not tbl:
+ # self.tables[table] = tbl = CacheTable(self.maxcount)
+ # tbl.set(sql, rows)
+
return rows
def reset(self, connection, table, fields = None):
assert(type(table) is str)
+ assert(not connection.readonly)
+ return
sql = None
if not fields is None:
sql = self.build_select(connection, table, fields)
diff --git a/db/connection.py b/db/connection.py
index 5ffd29f..0bb86f1 100644
--- a/db/connection.py
+++ b/db/connection.py
@@ -62,6 +62,8 @@ class Connection:
self.readonly = readonly
self.finished = True
self.begin()
+ self.on_commit = list()
+ self.on_rollback = list()
def parse(self, text, *args, **kvargs):
i = iter(text)
@@ -140,13 +142,38 @@ class Connection:
self.execute("START TRANSACTION READ WRITE")
self.now = datetime.datetime.now(datetime.timezone.utc)
+ def process_events(self, events, skip_errors = False):
+ while events:
+ events_copy = list(events)
+ events.clear()
+ for event in events_copy:
+ try:
+ event[0](*event[1], *event[2])
+ except Exception as e:
+ print("exception in event")
+ print(traceback.format_exc())
+ print(e)
+ if not skip_errors:
+ raise e
+
+ def call_on_commit(self, function, *args, **kvargs):
+ self.on_commit.append((function, args, kvargs))
+ def call_on_rollback(self, function, *args, **kvargs):
+ self.on_rollback.append((function, args, kvargs))
+
def commit(self):
assert not self.finished
+ self.process_events(self.on_commit)
+ self.on_commit.clear()
+ self.on_rollback.clear()
self.internal.commit()
self.finished = True
def rollback(self):
assert not self.finished
+ self.process_events(self.on_rollback, skip_errors = True)
+ self.on_commit.clear()
+ self.on_rollback.clear()
self.internal.rollback()
self.finished = True
diff --git a/db/types.py b/db/types.py
index 6d9213a..3120a99 100644
--- a/db/types.py
+++ b/db/types.py
@@ -40,7 +40,7 @@ class Int(Type):
class String(Type):
def from_raw(self, value):
- return str(value)
+ return '' if value is None else str(value)
def to_db(self, connection, value):
return "'" + connection.escape_string(self.from_raw(value)) + "'"
@@ -52,7 +52,7 @@ class Float(Type):
class Date(Type):
def from_raw(self, value):
- return str_to_date(date_to_str(value_raw) if type(value_raw) is datetime.datetime else str(value_raw))
+ return str_to_date(date_to_str(value) if type(value) is datetime.datetime else str(value))
def from_db(self, connection, value):
return set_timezone(value)
diff --git a/deps.txt b/deps.txt
index 2fac286..412c325 100644
--- a/deps.txt
+++ b/deps.txt
@@ -8,14 +8,19 @@ time
datetime
hashlib
+os
+subprocess
threading
traceback
### modules from python std library
+base64
cgi
html
+http.client
+shutil
urllib.parse
uuid
diff --git a/doc/plan.ods b/doc/plan.ods
index 70ee2ee..e682e2f 100644
Binary files a/doc/plan.ods and b/doc/plan.ods differ
diff --git a/generator/form.py b/generator/form.py
new file mode 100644
index 0000000..bf07aab
--- /dev/null
+++ b/generator/form.py
@@ -0,0 +1,105 @@
+
+
+class Form():
+ FORM_BEGIN = '
\n'
+ GROUP_BEGIN = '\n'
+ TITLE_BEGIN = '\n'
+ FIELD_BEGIN = ''
+ FIELD_END = '
\n'
+ NAME_BEGIN = ''
+ NAME_END = '\n'
+ VALUE_BEGIN = ''
+ VALUE_END = '
\n'
+
+ BEGIN = FORM_BEGIN + GROUP_BEGIN
+ END = GROUP_END + FORM_END
+
+ def __init__(self, request):
+ self.request = request
+ self.answer = self.request.answer
+ self.content = ''
+
+ def title(self, title):
+ return self.TITLE_BEGIN \
+ + self.answer.te(title) \
+ + self.TITLE_END
+
+ def name(self, name):
+ return self.NAME_BEGIN \
+ + self.answer.te(name) \
+ + self.NAME_END
+
+ def value_raw(self, raw):
+ return self.VALUE_BEGIN \
+ + raw \
+ + self.VALUE_END
+
+ def input(self, name, type, value = None):
+ type = self.answer.e(type)
+ name = self.answer.e(name)
+ value = self.answer.encodedfield(name) if value is None else self.answer.e(value)
+ content = ''
+ return content
+
+ def select(self, name, variants, value = None):
+ name = self.answer.e(name)
+ value = self.answer.encodedfield(name) if value is None else self.answer.e(value)
+ content = '\n'
+ return content
+
+ def textarea(self, name, value = None):
+ name = self.answer.e(name)
+ value = self.answer.encodedfield(name) if value is None else self.answer.e(value)
+ content = ''
+ return content
+
+ def field_raw(self, name, raw):
+ content = self.FIELD_BEGIN;
+ content += self.name(name)
+ content += self.value_raw(raw)
+ content += self.FIELD_END;
+ return content
+
+ def add_field_raw(self, name, raw):
+ self.content += self.field_raw(name, raw)
+
+ def add_hidden(self, name, value):
+ self.content += self.input(name, 'hidden', '' if value is None else value)
+ def add_input(self, title, *args, **kvargs):
+ self.add_field_raw(title, self.input(*args, **kvargs))
+ def add_select(self, title, *args, **kvargs):
+ self.add_field_raw(title, self.select(*args, **kvargs))
+ def add_textarea(self, title, *args, **kvargs):
+ self.add_field_raw(title, self.textarea(*args, **kvargs))
+ def add_submit(self, title = 'Submit'):
+ self.add_field_raw('', self.input('', 'submit', self.answer.t(title)))
+
+ def begin(self, title, action):
+ self.content += self.BEGIN
+ self.content += self.title(title)
+ if action: self.add_hidden('action', action)
+ def end(self):
+ self.content += self.END
+
diff --git a/main.py b/main.py
index 6027c8b..5deec1c 100644
--- a/main.py
+++ b/main.py
@@ -12,13 +12,17 @@ server = Server(config)
def application(env, start_response):
try:
request = Request(server, env)
- if request.path is None:
- raise exception.HttpNotFound()
if request.method != 'GET' and request.method != 'POST':
result = request.answer.complete_error('405 Method Not Allowed')
return result.complete(start_response)
+ if request.path is None:
+ raise exception.HttpNotFound()
+ if request.path and request.path[0] == 'repo':
+ result = request.server.repoproxy.process(request, request.path[1:])
+ return result.complete(start_response)
if request.method == 'POST':
+ request.read_postvars()
action = request.server.actions.get(request.action)
if not action:
diff --git a/model/model.py b/model/model.py
index 9e96e79..e1d6a70 100644
--- a/model/model.py
+++ b/model/model.py
@@ -2,6 +2,7 @@
from model.rights import MyRights
from model.rights import InternalRights
from model.users import Users
+from model.repositories import Repositories
class Model:
@@ -13,4 +14,11 @@ class Model:
self.translator = translator
self.users = Users(self)
+ self.repositories = Repositories(self)
+ def verify_path_entry(self, entry):
+ return type(entry) is str and len(entry) < 200 and entry.isalnum()
+
+ def verify_identifier(self, identifier):
+ return self.verify_path_entry(identifier) and identifier.isidentifier()
+
diff --git a/model/repositories.py b/model/repositories.py
new file mode 100644
index 0000000..41774ea
--- /dev/null
+++ b/model/repositories.py
@@ -0,0 +1,150 @@
+
+import exception
+
+from model.base import ModelBase
+
+
+class Repository(ModelBase):
+ def __init__(self, repositories, row, user = None):
+ self.repositories = repositories
+ super().__init__(self.repositories.model)
+
+ self.id = int(row['id'])
+ self.user_id = int(row['user_id'])
+ self.name = str(row['name'])
+ self.type = str(row['type'])
+ self.title = str(row['title'])
+ self.description = str(row['description'])
+
+ assert(not user or user.id == self.user_id)
+ self.user = user
+ self.repotype = self.repositories.repotypes[self.type]
+
+ def get_user(self):
+ if self.user == None:
+ self.user = self.model.users.get_by_id(self.user_id)
+ assert(self.user)
+ assert(self.user.id == self.user_id)
+ return self.user
+
+ def reset_cache(self):
+ self.repositories.reset_cache(self.id, self.user_id, self.name)
+
+ def gen_subpath(self):
+ return self.repositories.gen_subpath(self.get_user().login, self.name)
+
+ def gen_internalurl(self):
+ return self.repotype.gen_internalurl( self.gen_subpath() )
+
+ def can_update(self):
+ return self.user_id == self.rights.user_id or self.rights.issuperuser()
+
+ def update(self, title, description):
+ if self.can_update():
+ self.connection.execute(
+ 'UPDATE %T SET `title`=%s, `description`=%s WHERE `id`=%d',
+ self.repositories.TABLE, title, description, self.id )
+ self.reset_cache()
+ else:
+ raise exception.ModelDeny()
+
+ def can_delete(self):
+ return self.user_id == self.rights.user_id or self.rights.issuperuser()
+
+ def delete(self, password = None):
+ if self.can_delete():
+ self.connection.execute(
+ 'DELETE FROM %T WHERE `id`=%d',
+ self.repositories.TABLE, self.id )
+ self.reset_cache()
+ self.connection.call_on_commit(self.repotype.delete, self.gen_subpath())
+ else:
+ raise exception.ModelDeny()
+
+
+class Repositories(ModelBase):
+ TABLE = 'repositories'
+
+ def __init__(self, model):
+ super().__init__(model)
+ self.repotypes = self.server.repotypes
+
+ def reset_cache(self, id, user_id, name):
+ self.connection.cache.reset_row(self.TABLE, id)
+ self.connection.cache.reset(self.TABLE, {'user_id': user_id, 'name': name})
+
+ def verify_name(self, name):
+ return self.model.verify_identifier(name)
+
+ def gen_subpath(self, login, name):
+ assert( self.model.verify_path_entry(login) )
+ assert( self.model.verify_path_entry(name) )
+ return login + '/' + name
+
+ def can_create(self, user_id):
+ assert(type(user_id) is int and user_id)
+ return user_id == self.rights.user_id or self.rights.issuperuser()
+
+ def create(self, user_id, name, repotype, title, description, user = None):
+ assert(not user or user.id == user_id)
+ if not self.can_create(user_id):
+ raise exception.ModelDeny()
+ if not self.verify_name(name):
+ raise exception.ModelWrongData(self.t('Repository name is incorrect'))
+ if not type(repotype) is str or not repotype in self.repotypes:
+ raise exception.ModelWrongData(self.t('Repository type is incorrect'))
+ if self.get_by_name(user_id, name):
+ raise exception.ModelWrongData(self.t('Repository already exists'))
+
+ if not user:
+ user = self.model.users.get_by_id(user_id)
+ if not user:
+ raise exception.ModelWrongData(self.t('User not found'))
+
+ self.connection.execute(
+ 'INSERT INTO %T SET `user_id`=%d, `name`=%s, `type`=%s, `title`=%s, `description`=%s',
+ self.TABLE, user_id, name, repotype, title, description )
+ id = self.connection.insert_id()
+ self.reset_cache(id, user_id, name)
+
+ subpath = self.gen_subpath(user.login, name)
+ repotype_instance = self.repotypes[repotype]
+ self.connection.call_on_rollback(repotype_instance.delete, subpath)
+ repotype_instance.create(subpath)
+
+ return self.get_by_id(id, user)
+
+ def get_by_id(self, id, user = None):
+ assert(type(id) is int)
+ row = self.connection.cache.row(self.TABLE, id)
+ if not row:
+ return None
+ if not user:
+ user = self.model.users.get_by_id(row['user_id'])
+ if not user:
+ return None
+ return Repository(self, row, user)
+
+ def get_by_name(self, user_id, name, user = None):
+ assert(not user or user.id == user_id)
+ assert(type(user_id) is int)
+ assert(type(name) is str)
+
+ rows = self.connection.cache.select(self.TABLE, {'user_id': user_id, 'name': name})
+ if not rows or len(rows) > 1:
+ return None
+ row = rows[0]
+ if not user:
+ user = self.model.users.get_by_id(row['user_id'])
+ if not user:
+ return None
+ return Repository(self, row, user)
+
+ def get_list(self, user):
+ assert(user)
+ result = list()
+ rows = self.connection.query_dict('SELECT * FROM %T WHERE `user_id`=%d ORDER BY `name`', self.TABLE, user.id)
+ for row in rows:
+ result.append(Repository(self, row, user))
+ return result
+
diff --git a/model/users.py b/model/users.py
index 296718b..3b0dbba 100644
--- a/model/users.py
+++ b/model/users.py
@@ -88,7 +88,7 @@ class Users(ModelBase):
return hashlib.sha512(bytes(str(id) + '|' + str(salt) + '|' + password, 'utf8')).hexdigest()
def verify_login(self, login):
- return type(login) is str and len(login) < 200 and login.isidentifier
+ return self.model.identifier(login)
def can_create(self):
return self.rights.issuperuser() or self.server.config['users']['selfcreate']
@@ -115,8 +115,6 @@ class Users(ModelBase):
return False
row = rows[0]
id = int(row['id'])
- print(row)
- print(self.gen_password_hash(id, password))
if str(row['password']) == self.gen_password_hash(id, password):
return id
return 0
diff --git a/page/repo.py b/page/repo.py
new file mode 100644
index 0000000..b3dc1ee
--- /dev/null
+++ b/page/repo.py
@@ -0,0 +1,172 @@
+
+import exception
+from generator.form import Form
+
+from page.page import Page
+
+
+class RepoCreatePage(Page):
+ def process(self, request, path, prevpath):
+ user = request.answer.objects['user']
+ if path:
+ raise exception.HttpNotFound()
+ if not request.model.repositories.can_create(user.id):
+ raise exception.HttpNotFound()
+ answer = request.answer
+ answer.chain_title( answer.te('Create repository') )
+
+ form = Form(request)
+ form.begin('Create repository', 'repo.create')
+ form.add_hidden('user_id', user.id)
+ form.add_select('type:', 'type', { k: v.name for k, v in request.server.repotypes.items() })
+ form.add_input('name:', 'name', 'text')
+ form.add_input('title:', 'title', 'text')
+ form.add_textarea('description:', 'description')
+ form.add_submit()
+ form.end()
+ answer.content += form.content
+
+ return answer.complete_content()
+
+
+class RepoPage(Page):
+ def __init__(self):
+ super().__init__()
+ self.view = RepoViewPage()
+
+ def process(self, request, path, prevpath):
+ if not path:
+ raise exception.HttpNotFound()
+
+ user = request.answer.objects['user']
+ repo = None
+ if path[0].isdecimal():
+ repo = request.model.repositories.get_by_id(int(path[0]), user)
+ else:
+ repo = request.model.repositories.get_by_name(user.id, str(path[0]), user)
+ if not repo:
+ raise exception.HttpNotFound()
+
+ request.answer.objects['repo'] = repo
+ request.answer.chain_title( request.answer.e(repo.title) )
+ return self.view.sub_process(request, path, prevpath)
+
+
+class RepoViewPage(Page):
+ def __init__(self):
+ super().__init__()
+ self.edit = RepoUpdatePage()
+ self.delete = RepoDeletePage()
+
+ def process(self, request, path, prevpath):
+ repo = request.answer.objects['repo']
+
+ answer = request.answer
+ answer.chain_title( answer.e(repo.name) )
+
+ if path:
+ if path[0] == 'edit':
+ return self.edit.sub_process(request, path, prevpath)
+ if path[0] == 'delete':
+ return self.delete.sub_process(request, path, prevpath)
+ raise exception.HttpNotFound()
+
+ answer.content += '' + answer.te('Repositiry') + '
\n'
+ answer.content += '' + answer.te(repo.repotype.name) + '
\n'
+ answer.content += '' + answer.e(repo.name) + '
\n'
+ answer.content += '' + answer.e(repo.title) + '
\n'
+ answer.content += '' + answer.e(repo.description) + '
\n'
+ answer.content += '' + request.protocol + request.domain + '/repo/' + repo.gen_subpath() + '
\n'
+
+ if repo.can_update():
+ answer.content += '' + self.make_link(answer, prevpath + ['edit'], 'Edit repository') + '
\n'
+
+ return answer.complete_content()
+
+
+class RepoUpdatePage(Page):
+ def process(self, request, path, prevpath):
+ if path:
+ raise exception.HttpNotFound()
+
+ answer = request.answer
+ repo = answer.objects['repo']
+ if not repo.can_update():
+ raise exception.HttpNotFound()
+
+ answer.chain_title( answer.te('Edit repository') )
+
+ form = Form(request)
+ form.begin('Edit repository', 'repo.update')
+ form.add_hidden('repository_id', repo.id)
+ form.add_input('title:', 'title', 'text', repo.title)
+ form.add_textarea('description:', 'description', repo.description)
+ form.add_submit()
+ form.end()
+ answer.content += form.content
+
+
+ if repo.can_delete() and prevpath:
+ url = request.get_urlpath_escaped(prevpath[:-1] + ['delete'])
+ form = Form(request)
+ form.content += '