commit 5675bc57738ddb9982596fddcfc2dc03e364d140 Author: Dmitry Date: Fri May 21 15:19:11 2021 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7734fe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +.idea/ +__pycache__/ + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d5947c4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "webpub1c"] + path = webpub1c + url = https://github.com/b4tman/webpub1c.git diff --git a/brackets.py b/brackets.py new file mode 100644 index 0000000..ba2994d --- /dev/null +++ b/brackets.py @@ -0,0 +1,76 @@ + +from typing import Union, List, Iterator + +BOM = '\ufeff' +ParsingIterator = Iterator[Union[str, 'ParsingIterator']] +NestedStringList = Union[str, List['NestedStringList']] + + +def text_reader(filename: str, encoding: str = 'utf-8') -> Iterator[str]: + with open(filename, 'rt', encoding=encoding, buffering=1) as f: + while True: + chunk: str = f.read(1) + if 0 == len(chunk): + break + elif chunk.startswith(BOM): + continue + yield chunk + + +def brackets_parser(reader: Iterator[str], level: int = 0) \ + -> ParsingIterator: + cur_elem: str = "" + is_quoted: bool = False + for chunk in reader: + if '{' == chunk and not is_quoted: + if 0 == level: # skip document root, always look at first { + level += 1 + continue + nested = brackets_parser(reader, level + 1) + yield nested + # ensure we are done for nested + for _ in nested: + pass + continue + elif ',' == chunk and not is_quoted: + if 0 < len(cur_elem): + yield cur_elem + cur_elem = '' + continue + elif '}' == chunk and not is_quoted: + if 0 < len(cur_elem): + yield cur_elem + break + elif '"' == chunk: + is_quoted = not is_quoted + continue + elif '\n' == chunk or '\r' == chunk: + continue + cur_elem += chunk + + +def brackets_select(parser: ParsingIterator, selector: str = '') \ + -> NestedStringList: + if '' == selector: + l_selector = [] + elem = parser + else: + l_selector = selector.split('/') + cur_sel: str = l_selector.pop(0) + idx: int = int(cur_sel) + _, elem = next(filter(lambda x: x[0] == idx, enumerate(parser))) + + if 0 == len(l_selector): + if type(elem) == str: + return elem + else: + return [i if type(i) == str else brackets_select(i) for i in elem] + + return brackets_select(elem, '/'.join(l_selector)) + + +def get_infobases(filename: str) -> List[str]: + reader = text_reader(filename) + parser = brackets_parser(reader) + base_list = brackets_select(parser, '2')[1:] + return list(map(lambda x: x[1], base_list)) diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..a7dc719 --- /dev/null +++ b/config.yml @@ -0,0 +1,12 @@ +infobases: + server_file: test/1CV8Clst.lst +apache_restart_flagfile: test/apache_restart +apache_config: webpub1c/test/apache.cfg +vrd_path: webpub1c/test/vrds +dir_path: webpub1c/test/pubs +url_base: /1c +platform_path: /opt/1cv8/x86_64/current +ws_module: wsap24.so +vrd_params: + debug: + server_addr: localhost diff --git a/pub1c-rest.py b/pub1c-rest.py new file mode 100644 index 0000000..d175214 --- /dev/null +++ b/pub1c-rest.py @@ -0,0 +1,317 @@ +import logging +import os +from typing import List, Optional +import traceback + +import yaml +from flask import Flask, g +from flask_restful import Resource, Api, reqparse, abort +from pathvalidate import is_valid_filepath + +from brackets import get_infobases as br_get_infobases +from webpub1c.webpub1c import VRDConfig, ApacheConfig, urlpath_join + +apache_restart_flagfile = 'restart_apache' + + +class WebPub1C: + def __init__(self, config, verbose: bool = False): + level = logging.INFO if verbose else logging.WARNING + logging.basicConfig(level=level) + self._log = logging.getLogger("webpub1c") + self._log.setLevel(level) + + self._config = config + + vrd_params: Optional[VRDConfig] = self._config.get('vrd_params', None) + apache_config: str = self._config.get('apache_config', '') + self._vrd_path: str = self._config.get('vrd_path', '') + self._dir_path: str = self._config.get('dir_path', '') + self._url_base: str = self._config.get('url_base', '') + + self._apache_cfg = ApacheConfig(apache_config, self._vrd_path, + self._dir_path, self._url_base, + vrd_params) + + def _is_vrd_path_valid(self) -> bool: + return os.path.isdir(self._vrd_path) + + def _is_dir_path_valid(self) -> bool: + return os.path.isdir(self._dir_path) + + def _is_url_base_valid(self) -> bool: + return is_valid_filepath(self._url_base, platform='posix') and self._url_base.startswith('/') + + def _is_module_valid(self) -> bool: + if 'platform_path' not in self._config: + return False + if 'ws_module' not in self._config: + return False + return os.path.isfile(os.path.join(self._config['platform_path'], self._config['ws_module'])) + + def check(self): + return { + 'config': self._config, + 'is_apache_cfg_valid': self._apache_cfg.is_valid(), + 'is_vrd_path_valid': self._is_vrd_path_valid(), + 'is_dir_path_valid': self._is_dir_path_valid(), + 'is_url_base_valid': self._is_url_base_valid(), + 'is_module_valid': self._is_module_valid() + } + + def has_module(self): + return self._apache_cfg.has_1cws_module() + + def add_module(self): + """ Add 1cws module to apache config """ + + if self._apache_cfg.has_1cws_module(): + self._log.info('config unchanged') + else: + module: str = os.path.join(self._config['platform_path'], self._config['ws_module']) + self._apache_cfg.add_1cws_module(module) + self._log.info('module added') + + def list(self) -> List[str]: + """ List publications """ + + return list(self._apache_cfg.publications) + + def get(self, ibname: str): + """ Get publication info """ + + publication = self._apache_cfg.get_publication(ibname) + if publication is None: + return publication + + return { + 'name': publication.name, + 'url': publication.url_path, + 'directory': publication.directory, + 'vrd_filename': publication.vrd_filename, + } + + def add(self, ibname: str, url: Optional[str] = None) -> str: + """ Add new publication """ + + publication = self._apache_cfg.create_publication(ibname, url) + self._apache_cfg.add_publication(publication) + self._log.info(f'publication added: {ibname}') + return publication.url_path + + def set_url(self, ibname: str, url: str) -> None: + """ Set publication url """ + + publication = self._apache_cfg.get_publication(ibname) + if publication is None: + raise KeyError(f'infobase "{ibname}" not publicated') + + publication.url_path = urlpath_join(self._url_base, url) + self._apache_cfg.remove_publication(publication.name, destroy=False) + self._apache_cfg.add_publication(publication) + self._log.info(f'publication changed: {ibname}') + + def remove(self, ibname: str): + """ Remove publication """ + + self._apache_cfg.remove_publication(ibname) + self._log.info(f'publication removed: {ibname}') + + +pub_parser = reqparse.RequestParser() +pub_parser.add_argument('name', required=True, type=str, help='name required') +pub_parser.add_argument('url', type=str) + +url_parser = reqparse.RequestParser() +url_parser.add_argument('url', type=str) + +url_req_parser = reqparse.RequestParser() +url_req_parser.add_argument('url', type=str, required=True) + + +def load_config(filename: str = 'config.yml'): + with open(filename, 'r', encoding='utf-8') as cfg_file: + return yaml.safe_load(cfg_file) + + +def get_config(): + if 'config' not in g: + g.config = load_config() + + return g.config + + +def get_webpub1c(): + if 'webpub1c' not in g: + g.webpub1c = WebPub1C(get_config()) + + return g.webpub1c + + +def load_infobases(config) -> List[str]: + result = [] + if 'infobases' in config and 'server_file' in config['infobases']: + result = br_get_infobases(config['infobases']['server_file']) + return result + + +def validate_url(url: Optional[str]): + if url is not None: + if not is_valid_filepath(url, platform='posix'): + abort(400, message='invalid url') + + +class InfobasesAvaible(Resource): + def get(self) -> List[str]: + cfg = get_config() + bases = load_infobases(cfg) + return bases + + +class ConfigTest(Resource): + def get(self): + webpub = get_webpub1c() + return webpub.check() + + +class Publications(Resource): + def get(self) -> List[str]: + webpub = get_webpub1c() + return webpub.list() + + def put(self): + args = pub_parser.parse_args() + validate_url(args.url) + webpub = get_webpub1c() + url = args.url + if args.name in webpub.list(): + abort(409, message=f'publication exists: {args.name}') + try: + url = webpub.add(args.name, url) + except Exception as e: + abort(422, message=f'publication failed: {args.name}', traceback=traceback.format_exc()) + return {'message': 'created', 'name': args.name, 'url': url}, 201 + + +class Publication(Resource): + def get(self, name: str): + webpub = get_webpub1c() + if name not in webpub.list(): + abort(404, message=f'publication not found: {name}') + return { + name: webpub.get(name) + } + + def put(self, name: str): + args = url_parser.parse_args() + validate_url(args.url) + webpub = get_webpub1c() + url = args.url + if name in webpub.list(): + abort(409, message=f'publication exists: {name}') + try: + url = webpub.add(name, url) + except Exception as e: + abort(422, message=f'publication failed: {name}', traceback=traceback.format_exc()) + return {'message': 'created', 'name': name, 'url': url}, 201 + + def delete(self, name: str): + webpub = get_webpub1c() + if name not in webpub.list(): + abort(404, message=f'publication not found: {name}') + try: + webpub.remove(name) + except Exception as e: + abort(422, message='delete failed', traceback=traceback.format_exc()) + return {'message': 'deleted', 'name': name} + + +class PublicationURL(Resource): + def get(self, name: str): + webpub = get_webpub1c() + if name not in webpub.list(): + abort(404, message=f'publication not found: {name}') + return { + 'url': webpub.get(name)['url'] + } + + def post(self, name: str): + args = url_req_parser.parse_args() + validate_url(args.url) + webpub = get_webpub1c() + url = args.url + if name not in webpub.list(): + abort(404, message=f'publication not found: {name}') + try: + webpub.set_url(name, url) + except Exception as e: + abort(422, message=f'set url failed', traceback=traceback.format_exc()) + return {'message': 'success', 'name': name, 'url': url} + + +class EnterpriseModule(Resource): + def get(self): + webpub = get_webpub1c() + if not webpub.has_module(): + abort(404, message='not found') + return { + 'message': 'found' + } + + def put(self): + webpub = get_webpub1c() + if webpub.has_module(): + abort(304, message='already added') + webpub.add_module() + return { + 'message': 'success' + } + + +class ApacheRestartFlag(Resource): + def get(self): + cfg = get_config() + flagfile = cfg.get('apache_restart_flagfile', apache_restart_flagfile) + if not os.path.isfile(flagfile): + abort(404, message='not found') + return { + 'message': 'found' + } + + def put(self): + cfg = get_config() + flagfile = cfg.get('apache_restart_flagfile', apache_restart_flagfile) + if os.path.isfile(flagfile): + abort(304, message='found') + with open(flagfile, 'a'): + pass + return { + 'message': 'success' + } + + +class APIIndex(Resource): + def get(self): + return ['infobases', 'publications', + 'module', 'config', 'apache_restart'] + + +app = Flask(__name__) +api = Api(app, '/api/v1/') + +api.add_resource(InfobasesAvaible, '/infobases') +api.add_resource(Publications, '/publications') +api.add_resource(Publication, '/publications/') +api.add_resource(PublicationURL, '/publications//url') +api.add_resource(EnterpriseModule, '/module') +api.add_resource(ConfigTest, '/config') +api.add_resource(ApacheRestartFlag, '/apache_restart') +api.add_resource(APIIndex, '/') + +with app.app_context(): + get_config() + get_webpub1c() + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..93f0c53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask==2.0.0 +Flask-RESTful==0.3.9 +jinja2==2.11.3 +PyYAML==5.4.1 +pathvalidate==2.4.1 +transliterate==1.10.2 +fire==0.4.0 \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..4828a5b --- /dev/null +++ b/test.py @@ -0,0 +1,21 @@ +from requests import put, get, post, delete +import json +from pprint import pformat + +api='http://localhost:5000/api/v1/' + + +def main(): + #print(api, get(api).json()) + #print(api+'infobases', get(api+'infobases').json()) + #print(api+'publications', get(api+'publications').json()) + res = put(api+'publications', data={'name': 'test3'}) + print('put', api+'publications', res.status_code, pformat(res.json())) + res = delete(api + 'publications/test3', data={'name': 'test3'}) + print('delete', api + 'publications/test3', res.status_code, pformat(res.json())) + #res = get(api + 'publications/test1') + #print('GET', api + 'publications/test1', res.status_code, pformat(res.json())) + + +if __name__ == '__main__': + main() diff --git a/test/1CV8Clst.lst b/test/1CV8Clst.lst new file mode 100644 index 0000000..db68aff --- /dev/null +++ b/test/1CV8Clst.lst @@ -0,0 +1,46 @@ +{0, +{17b7beb0-144d-4d6b-b043-bdf0c0eab094,"Локальный кластер",1541,"srv1c",0,0,0,60,0,0,0, +{1, +{"srv1c",1541} +},0,0,1,0}, +{3, +{cc7cc0f2-fb7a-4ff3-adc9-3467f8a99143,"bpdemo","","PostgreSQL","srv1c","bpdemo","postgres","Im4P/VU4eWiYDWCH7LH1JWJn6t9BNlQXxh0uGt75q0c=","CrSQLDB=Y;DB=bpdemo;DBMS=PostgreSQL;DBSrvr=srv1c;DBUID=postgres;LIC=4106372409556202483;LicDstr=Y;Locale=ru_RU;Ref=bpdemo;SchJobDn=Y;SLev=0;Srvr=srv1c",0, +{0,00010101000000,00010101000000,"","","",0},1,1,"",0,"","",6,0}, +{9215f750-d5d0-412e-8cb3-b3d6d0ded6cb,"test1","","PostgreSQL","srv1c","test1","postgres","Im4P/VU4eWiYDWCH7LH1JXNW0Cie+5zSwb2n3eaq66I=","CrSQLDB=Y;DB=test1;DBMS=PostgreSQL;DBSrvr=srv1c;DBUID=postgres;LIC=2306107153984533500;LicDstr=Y;Locale=ru_RU;Ref=test1;SchJobDn=Y;SLev=0;Srvr=srv1c",0, +{0,00010101000000,00010101000000,"","","",0},1,1,"",0,"","",7,0}, +{9d87ba92-e5f7-472d-beb0-f7eaa1a9cc54,"test2","","PostgreSQL","srv1c","test2","postgres","Im4P/VU4eWiYDWCH7LH1JYz5cBkpoT8qfQtYmLS0PYc=","CrSQLDB=Y;DB=test2;DBMS=PostgreSQL;DBSrvr=srv1c;DBUID=postgres;LIC=4593938853856542963;LicDstr=Y;Locale=ru_RU;Ref=test2;SchJobDn=Y;SLev=0;Srvr=srv1c",0, +{0,00010101000000,00010101000000,"","","",0},1,1,"",0,"","",8,0} +}, +{1, +{b927a7dd-e12e-46ab-b1cb-33f2993953ee,"srv1c",1,0,1000,42466209-1c41-46f4-98c2-90115eba0fa0,0} +}, +{0}, +{1, +{42466209-1c41-46f4-98c2-90115eba0fa0,"Центральный сервер",1540,"srv1c",1, +{1, +{1560,1591} +},"","zMjOccNgmPlsungP/Xu+4A==",0,0,8,256,1000,1,0,1,0,1,1541,0,0,300} +}, +{1, +{009e91fe-c690-4368-89fd-09d2480dc782,"Главный менеджер кластера","srv1c",1,1,42466209-1c41-46f4-98c2-90115eba0fa0} +}, +{0}, +{0}, +{0}, +{0}, +{0}, +{0,0},0, +{0}, +{0}, +{0}, +{0}, +{0}, +{0}, +{0}, +{0}, +{0},9, +{0}, +{0}, +{""}, +{""}, +{0},"/6GLe5mdD+sXX+7cLLHLbu6Wrr3MyGtKKjXj7YVu9sk="} \ No newline at end of file diff --git a/webpub1c b/webpub1c new file mode 160000 index 0000000..9a60a27 --- /dev/null +++ b/webpub1c @@ -0,0 +1 @@ +Subproject commit 9a60a27b36cc62b930cb1075e985cf88663ac095