initial commit
This commit is contained in:
commit
5675bc5773
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
venv/
|
||||||
|
.idea/
|
||||||
|
__pycache__/
|
||||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "webpub1c"]
|
||||||
|
path = webpub1c
|
||||||
|
url = https://github.com/b4tman/webpub1c.git
|
76
brackets.py
Normal file
76
brackets.py
Normal file
@ -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))
|
12
config.yml
Normal file
12
config.yml
Normal file
@ -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
|
317
pub1c-rest.py
Normal file
317
pub1c-rest.py
Normal file
@ -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/<string:name>')
|
||||||
|
api.add_resource(PublicationURL, '/publications/<string:name>/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)
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -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
|
21
test.py
Normal file
21
test.py
Normal file
@ -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()
|
46
test/1CV8Clst.lst
Normal file
46
test/1CV8Clst.lst
Normal file
@ -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="}
|
1
webpub1c
Submodule
1
webpub1c
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 9a60a27b36cc62b930cb1075e985cf88663ac095
|
Loading…
Reference in New Issue
Block a user