data/i18n/main.lqa
*~
data/i18n/lokalize-scripts/
+rest_server/tools/rest_server
RegExp? regExpExcluded;
// ...........................1.............1.........2
final regExpModule =
- RegExp(r'''(\w+) = (I18N\(\)|i18N)\.module\(["'](.*?)['"]\);''');
+ RegExp(r'''(\w+) = (I18N\(\)|i18n)\.module\(["'](.*?)['"]\);''');
final regExpDelimiter = RegExp(r'''["']''');
// ..........1.............1..2..............................2
- final regExpText = RegExp(r'(i18N|I18N\(\))\.(tr|ntr|trMulti|trArgs)\(');
+ final regExpText = RegExp(r'(i18n|I18N\(\))\.(tr|ntr|trMulti|trArgs)\(');
final regExpStringConstant = RegExp(r'''^\s*(r?)(["'])(.*?)\2''');
final regExpVariable = RegExp(r'^\w+');
final regExpEmptyString = RegExp(r'^\s*$');
--- /dev/null
+{
+ "form.card.padding": "16.0",
+ "form.gap.field_button.height": "16.0",
+ "productive": {
+ "version": "1.0",
+ "host": "localhost",
+ "port": 6000,
+ "scheme": "http",
+ "timeout": 10
+ },
+ "development": {
+ "version": "1.0",
+ "host": "pollsam.hamatoma.de",
+ "port": 443,
+ "scheme": "https",
+ "timeout": 10
+ },
+ "configDirectory": ".",
+ "dataDirectory": "."
+}
+enum ServerEnvironment { productive, development }
typedef YamlMap = Map<String, dynamic>;
typedef YamlList = List<YamlMap>;
import 'package:dart_bones/dart_bones.dart';
typedef MapModule = Map<String, String>;
typedef MapPlural = Map<String, PluralInfo>;
-class I18n {
- static I18n? instance;
+class I18N {
+ static I18N? instance;
final BaseLogger logger;
final globalModule = '!global';
Map<String, MapModule> mapModules = {};
String locale = 'de_DE';
String loadedLocale = '';
final regExpTag = RegExp(r'<#\d+>$');
- factory I18n() {
+ factory I18N() {
return instance!;
}
- I18n.internal(this.logger) {
+ I18N.internal(this.logger) {
instance = this;
}
import 'i18n.dart';
-class I18nIo extends I18n {
+class I18nIo extends I18N {
String dataDirectory;
final regExpId = RegExp(r'^msgid "(.+?)"$');
final regExpTranslation = RegExp(r'^msgstr "(.+?)"$');
--- /dev/null
+import 'package:dart_bones/dart_bones.dart';
+import 'package:exhibition/page/start_page.dart';
+import 'package:flutter/material.dart';
+import 'page/users/users_list_page.dart';
+import 'page/info_page.dart';
+import 'page/log_page.dart';
+
+import 'setting/global_data.dart';
+
+class ExhibitionApp extends StatefulWidget {
+ static BaseLogger logger = globalLogger;
+
+ @override
+ ExhibitionAppState createState() {
+ return ExhibitionAppState();
+ }
+}
+
+class ExhibitionAppState extends State<ExhibitionApp> {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ debugShowCheckedModeBanner: true,
+ title: 'Exhibition',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+ ),
+ initialRoute: '/start',
+ onGenerateRoute: _getRoute,
+ );
+ }
+}
+
+Route<dynamic>? _getRoute(RouteSettings settings) {
+ MaterialPageRoute? route;
+ StatefulWidget? page;
+ switch (settings.name) {
+ case '/users/list':
+ page = UsersListPage(GlobalData());
+ break;
+ case '/start':
+ page = StartPage(globalLogger);
+ break;
+ case '/info':
+ page = InfoPage(GlobalData());
+ break;
+ case '/log':
+ page = LogPage(GlobalData());
+ break;
+ }
+ if (page != null) {
+ route = MaterialPageRoute<void>(
+ settings: settings,
+ builder: (BuildContext context) => page!,
+ fullscreenDialog: false,
+ );
+ }
+ return route;
+}
+import 'package:dart_bones/dart_bones.dart';
+import 'package:exhibition/base/i18n.dart';
+import 'package:exhibition/setting/global_data.dart';
import 'package:flutter/material.dart';
-
-void main() {
- runApp(const MyApp());
-}
-
-class MyApp extends StatelessWidget {
- const MyApp({Key? key}) : super(key: key);
-
- // This widget is the root of your application.
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- // This is the theme of your application.
- //
- // Try running your application with "flutter run". You'll see the
- // application has a blue toolbar. Then, without quitting the app, try
- // changing the primarySwatch below to Colors.green and then invoke
- // "hot reload" (press "r" in the console where you ran "flutter run",
- // or simply save your changes to "hot reload" in a Flutter IDE).
- // Notice that the counter didn't reset back to zero; the application
- // is not restarted.
- primarySwatch: Colors.blue,
- ),
- home: const MyHomePage(title: 'Flutter Demo Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({Key? key, required this.title}) : super(key: key);
-
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
- final String title;
-
- @override
- State<MyHomePage> createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
- int _counter = 0;
-
- void _incrementCounter() {
- setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
- _counter++;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
- return Scaffold(
- appBar: AppBar(
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
- title: Text(widget.title),
- ),
- body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
- child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Invoke "debug painting" (press "p" in the console, choose the
- // "Toggle Debug Paint" action from the Flutter Inspector in Android
- // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
- // to see the wireframe for each widget.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- const Text(
- 'You have pushed the button this many times:',
- ),
- Text(
- '$_counter',
- style: Theme.of(context).textTheme.headline4,
- ),
- ],
- ),
- ),
- floatingActionButton: FloatingActionButton(
- onPressed: _incrementCounter,
- tooltip: 'Increment',
- child: const Icon(Icons.add),
- ), // This trailing comma makes auto-formatting nicer for build methods.
- );
- }
+import 'exhibition_app.dart';
+import 'persistence/rest_persistence.dart';
+
+void main() async {
+ final logger = MemoryLogger(LEVEL_FINE);
+ ExhibitionApp.logger = logger;
+ I18N.internal(logger);
+ final configuration = BaseConfiguration({}, logger);
+ // We create a simplified instance. The real instance will be created in StartPage
+ GlobalData.internal(
+ configuration,
+ (p0) => null,
+ (p0) => null,
+ () => null,
+ RestPersistence(
+ version: '0.0.1',
+ host: '',
+ port: 0,
+ sessionTimeout: 30,
+ logger: logger),
+ logger);
+ runApp(ExhibitionApp());
}
import 'module_meta_data.dart';
import '../base/i18n.dart';
-final i18N = I18n();
-final M = i18N.module("Roles");
+final i18n = I18N();
+final M = i18n.module("Roles");
class RoleMeta extends ModuleMetaData {
static RoleMeta instance = RoleMeta.internal();
'Roles',
[
PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
- i18N.tr('Id')),
+ i18n.tr('Id')),
PropertyMetaData(
- 'name', DataType.string, ':notnull:', '', i18N.tr('Name'),
+ 'name', DataType.string, ':notnull:', '', i18n.tr('Name'),
size: 64),
PropertyMetaData('created', DataType.datetime, ':hidden:', '',
- i18N.tr('Created')),
+ i18n.tr('Created')),
PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
- i18N.tr('Created by'),
+ i18n.tr('Created by'),
size: 32),
PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
- i18N.tr('Changed')),
+ i18n.tr('Changed')),
PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
- i18N.tr('Changed by'),
+ i18n.tr('Changed by'),
size: 32),
],
);
import '../base/defines.dart';
import 'module_meta_data.dart';
import '../base/i18n.dart';
-final i18N = I18n();
-final M = i18N.module("Users");
+final i18n = I18N();
+final M = i18n.module("Users");
class UserMeta extends ModuleMetaData {
static UserMeta instance = UserMeta.internal();
'Users',
[
PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
- i18N.tr('Id')),
+ i18n.tr('Id')),
PropertyMetaData('name', DataType.string, ':notnull:', '',
- i18N.tr('Name'),
+ i18n.tr('Name'),
size: 64),
PropertyMetaData('displayName', DataType.string, ':unique:notnull:',
- '', i18N.tr('Display name', M),
+ '', i18n.tr('Display name', M),
size: 32),
PropertyMetaData('email', DataType.string, ':unique:notnull:', '',
- i18N.tr('EMail', M),
+ i18n.tr('EMail', M),
size: 255),
PropertyMetaData('role', DataType.reference, ':notnull:', '',
- i18N.tr('Role'), reference: 'roles.role_id'),
+ i18n.tr('Role'), reference: 'roles.role_id'),
PropertyMetaData('created', DataType.datetime, ':hidden:', '',
- i18N.tr('Created')),
+ i18n.tr('Created')),
PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
- i18N.tr('Created by'),
+ i18n.tr('Created by'),
size: 32),
PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
- i18N.tr('Changed')),
+ i18n.tr('Changed')),
PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
- i18N.tr('Changed by'),
+ i18n.tr('Changed by'),
size: 32),
],
);
--- /dev/null
+import 'dart:convert' as convert;
+
+import 'package:dart_bones/dart_bones.dart';
+import 'package:exhibition/persistence/rest_persistence.dart';
+import 'package:exhibition/setting/app_bar_exhibition.dart';
+import 'package:exhibition/setting/drawer_exhibition.dart';
+import 'package:exhibition/setting/footer_exhibition.dart';
+import 'package:flutter/material.dart';
+
+//import '../helper/exhibition_defines.dart';
+import '../setting/installation.dart';
+import '../setting/global_data.dart';
+
+class StartPage extends StatefulWidget {
+ final BaseLogger logger;
+
+ StartPage(this.logger, {Key? key}) : super(key: key);
+
+ @override
+ StartPageState createState() {
+ // UserPageState.setPageData(pageData);
+ final rc = StartPageState(logger);
+ logger.log('Start');
+ return rc;
+ }
+}
+
+class StartPageState extends State<StartPage> {
+ final BaseLogger logger;
+ final GlobalKey<FormState> _formKey =
+ GlobalKey<FormState>(debugLabel: 'Start');
+
+ String logName = '';
+
+ StartPageState(this.logger);
+
+ @override
+ Widget build(BuildContext context) {
+ final padding = 16.0;
+ final listItems = <Widget>[
+ Text(
+ 'Herzlich Willkommen',
+ style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ SizedBox(height: padding),
+ ...(logger as MemoryLogger)
+ .messages
+ .map((line) => Text(
+ line,
+ style: TextStyle(
+ color: line.startsWith('+++') ? Colors.red : Colors.black),
+ ))
+ .toList(),
+ SizedBox(height: padding),
+ ElevatedButton(
+ onPressed: () => setState(() => 1), child: Text('Aktualisieren')),
+ ];
+ return Scaffold(
+ //appBar: applicationData.appBarBuilder('Start'),
+ //drawer: applicationData.drawerBuilder(context),
+ body: Form(
+ key: _formKey,
+ child: Card(
+ margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+ child: Padding(
+ padding:
+ EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+ child: ListView(
+ children: listItems,
+ ))),
+ ));
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ Installation.initialize(logger);
+ // Note: Installation.kind must be set before.
+ final installation = Installation();
+ installation.checkPermissions().then((success) {
+ setState(() {
+ if (success) {
+ setState(() {
+ installation.checkConfiguration().then((success) {
+ if (success) {
+ logger.log(i18n.tr('Starting application'));
+ final content = FileSync()
+ .fileAsString(installation.configurationFile.path);
+ final map = convert.jsonDecode(content);
+ final configuration = BaseConfiguration(map, logger);
+ final globalData = GlobalData.internal(
+ configuration,
+ AppBarExhibition.builder(),
+ DrawerExhibition.builder(),
+ FooterExhibition.builder(),
+ RestPersistence.fromConfig(configuration, logger),
+ logger);
+ globalData.initializeAsync().then((value) {
+ Navigator.pushNamed(context, '/users/list');
+ });
+ }
+ });
+ });
+ };
+ });
+ });
+ }
+
+ void store(context) async {
+ if (_formKey.currentState!.validate()) {
+ _formKey.currentState!.save();
+ }
+ }
+}
+++ /dev/null
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../base/defines.dart';
-import 'user_state.dart';
-import 'package:equatable/equatable.dart';
-
-class User extends StatefulWidget {
- @override
- _UserState createState() => _UserState();
-}
-
-class _UserState extends State<User> {
- double? width;
-
- @override
- void initState() {
- //context.read<UserBloc>().add(UserEvent(EventSource.undef, EventCardinality.undef));
- super.initState();
- }
-
- @override
- void didChangeDependencies() {
- width = MediaQuery.of(context).size.width;
- super.didChangeDependencies();
- }
-
- @override
- Widget build(BuildContext context) {
- return BlocBuilder<UserBloc, UserState>(
- builder: (context, UserState state) {
- return Column(
- children: [
- //ResultDisplay(
- // text: _getDisplayText(state.calculationModel),
- //),
- Row(
- children: [
- _getButton(text: '7', onTap: () => numberPressed(7)),
- _getButton(text: '8', onTap: () => numberPressed(8)),
- _getButton(text: '9', onTap: () => numberPressed(9)),
- _getButton(
- text: 'x',
- onTap: () => operatorPressed('*'),
- backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
- ],
- ),
- Row(
- children: [
- _getButton(text: '4', onTap: () => numberPressed(4)),
- _getButton(text: '5', onTap: () => numberPressed(5)),
- _getButton(text: '6', onTap: () => numberPressed(6)),
- _getButton(
- text: '/',
- onTap: () => operatorPressed('/'),
- backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
- ],
- ),
- Row(
- children: [
- _getButton(text: '1', onTap: () => numberPressed(1)),
- _getButton(text: '2', onTap: () => numberPressed(2)),
- _getButton(text: '3', onTap: () => numberPressed(3)),
- _getButton(
- text: '+',
- onTap: () => operatorPressed('+'),
- backgroundColor: Color.fromRGBO(220, 220, 220, 1))
- ],
- ),
- Row(
- children: [
- _getButton(
- text: '=',
- onTap: calculateResult,
- backgroundColor: Colors.orange,
- textColor: Colors.white),
- _getButton(text: '0', onTap: () => numberPressed(0)),
- _getButton(
- text: 'C',
- onTap: clear,
- backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
- _getButton(
- text: '-',
- onTap: () => operatorPressed('-'),
- backgroundColor: Color.fromRGBO(220, 220, 220, 1)),
- ],
- ),
- Spacer(),
- //UserHistoryContainer(
- // calculations: state.history.reversed.toList())
- ],
- );
- },
- );
- }
-
- Widget _getButton(
- {required String text,
- required void Function() onTap,
- Color backgroundColor = Colors.white,
- Color textColor = Colors.black}) {
- /*
- return CalculatorButton(
- label: text,
- onTap: onTap,
- // @ToDo: width always != null?
- size: width! / 4 - 12,
- backgroundColor: backgroundColor,
- labelColor: textColor,
- );
- */
- return SizedBox(width: 10);
- }
-
- numberPressed(int number) {
- //context.read<UserBloc>().add(NumberPressed(number: number));
- }
-
- operatorPressed(String operator) {
- //context.read<UserBloc>().add(OperatorPressed(operator: operator));
- }
-
- calculateResult() {
- //context.read<UserBloc>().add(CalculateResult());
- }
-
- clear() {
- //context.read<UserBloc>().add(ClearUser());
- }
- /*
- String _getDisplayText(UserModel model) {
- if (model.result != null) {
- return '${model.result}';
- }
-
- if (model.secondOperand != null) {
- return '${model.firstOperand}${model.operator}${model.secondOperand}';
- }
-
- if (model.operator != null) {
- return '${model.firstOperand}${model.operator}';
- }
-
- if (model.firstOperand != null) {
- return '${model.firstOperand}';
- }
-
- return "${model.result ?? 0}";
- }
- */
-}
--- /dev/null
+import 'package:exhibition/setting/global_data.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../base/defines.dart';
+import 'user_state.dart';
+import 'package:equatable/equatable.dart';
+import '../../base/i18n.dart';
+final i18n = I18N();
+class UsersListPage extends StatefulWidget {
+ final GlobalData globalData;
+ UsersListPage(this.globalData): super();
+ @override
+ _UsersListPageState createState() => _UsersListPageState();
+}
+
+class _UsersListPageState extends State<UsersListPage> {
+ double? width;
+
+ @override
+ void initState() {
+ //context.read<UserBloc>().add(UserEvent(EventSource.undef, EventCardinality.undef));
+ super.initState();
+ }
+
+ @override
+ void didChangeDependencies() {
+ width = MediaQuery.of(context).size.width;
+ super.didChangeDependencies();
+ }
+ final nameController = TextEditingController();
+ @override
+ Widget build(BuildContext context) {
+ final padding = 16.0;
+ return BlocBuilder<UserBloc, UserState>(
+ builder: (context, UserState state) {
+ return Column(
+ children: [
+ TextFormField(
+ controller: nameController,
+ readOnly: true,
+ decoration: InputDecoration(labelText: i18n.tr('Name')),
+ ),
+ SizedBox(height: padding),
+ Text('Liste'),
+ Spacer(),
+ //UserHistoryContainer(
+ // calculations: state.history.reversed.toList())
+ ],
+ );
+ },
+ );
+ }
+
+}
+import 'package:exhibition/setting/global_data.dart';
import 'package:flutter/material.dart';
class AppBarExhibition extends AppBar {
AppBarExhibition({required String title, Key? key})
: super(title: Text(title), key: key);
- static AppBarExhibition builder(String title) =>
- AppBarExhibition(title: title);
+ static AppBarBuilder builder() =>
+ (String title) => AppBarExhibition(title: title);
}
: super(child: buildGrid(context), key: key);
/// Returns a method creating a drawer.
- static DrawerExhibition builder(dynamic context) => DrawerExhibition(context);
+ static DrawerBuilder builder() => (context) => DrawerExhibition(context);
static Widget buildGrid(context) {
final converter = MenuConverter();
}
/// Returns a method creating a footer.
- static FooterExhibition builder() => FooterExhibition();
+ static FooterBuilder builder() => () => FooterExhibition();
}
import 'package:exhibition/persistence/rest_persistence.dart';
import 'package:flutter/material.dart';
+import '../base/defines.dart';
import '../page/page_controller_exhibition.dart';
typedef AppBarBuilder = Function(String);
Widget widget(PageControllerExhibition controller);
}
-/// Storage for globale resources. This is a singleton.
+/// Storage for global resources. This is a singleton.
class GlobalData {
- static const version = '2021.06.027.00';
+ static const version = '2021.08.24.00';
static GlobalData? _instance;
+ static String baseDirectory = '';
+ static var serverEnvironment = ServerEnvironment.productive;
+
final BaseLogger logger;
final AppBarBuilder appBarBuilder;
final DrawerBuilder drawerBuilder;
this.drawerBuilder, this.footerBuilder,
this.restPersistence, this.logger) {
logger.log('Start');
+ _instance = this;
+ }
+ Future initializeAsync() async{
+ // Do customize!
+ return;
}
}
--- /dev/null
+import 'dart:async';
+import 'dart:convert' as convert;
+import 'dart:io';
+
+import 'package:dart_bones/dart_bones.dart';
+import 'package:path/path.dart' as path;
+import 'package:permission_handler/permission_handler.dart';
+
+import 'global_data.dart';
+import '../base/defines.dart';
+import '../base/i18n.dart';
+
+final i18n = I18N();
+final M = i18n.module('Example');
+
+/// Initializes the program if needed.
+class Installation {
+ static String osType = Platform.operatingSystem;
+ static Installation? instance;
+ static ServerEnvironment serverEnvironment = ServerEnvironment.productive;
+ static int retries = 1;
+ final neededPermissions = <Permission>[
+ Permission.camera,
+ Permission.location,
+ Permission.storage,
+ Permission.manageExternalStorage,
+ ];
+ bool _success = true;
+ Directory home = Directory('.');
+ Directory dataDirectory = Directory('.');
+ File configurationFile = File('');
+ File hivesFile = File('');
+ File dataFile = File('');
+ String user = '?';
+ final BaseLogger logger;
+
+ factory Installation() => instance ?? Installation.internal(globalLogger);
+
+ /// The private constructor.
+ /// [logger] does the logging.
+ Installation.internal(this.logger) {
+ logger.log('Version: ${GlobalData.version}');
+ }
+
+ bool get success => _success;
+
+ /// Checks whether there is a valid configuration file.
+ /// If not we try to create one.
+ /// Returns true: a valid configuration exists.
+ Future<bool> checkConfiguration() async {
+ bool forceInit = false;
+ if (GlobalData.baseDirectory.isEmpty) {
+ GlobalData.baseDirectory = home.path;
+ }
+ configurationFile = File(path.join(home.path, 'exhibition.json'));
+ if (!(forceInit ||
+ !configurationFile.existsSync() ||
+ !hasValidConfiguration())) {
+ logger.log(i18n.tr('Configuration OK'), LEVEL_DETAIL);
+ } else {
+ final content = '''{
+ "form.card.padding": "16.0",
+ "form.gap.field_button.height": "16.0",
+ "productive": {
+ "version": "1.0",
+ "host": "localhost",
+ "port": 6000,
+ "scheme": "http",
+ "timeout": 10
+ },
+ "development": {
+ "version": "1.0",
+ "host": "pollsam.hamatoma.de",
+ "port": 443,
+ "scheme": "https",
+ "timeout": 10
+ },
+ "configDirectory": "${home.path}",
+ "dataDirectory": "${dataDirectory.path}"
+}
+''';
+ logger.log(
+ i18n.tr('creating configuration:') + ' ${configurationFile.path}');
+ configurationFile.writeAsStringSync(content, mode: FileMode.writeOnly);
+ }
+ var rc = hasValidConfiguration();
+ if (rc) {
+ // do needed things!
+ }
+ if (rc) {
+ logger.log(i18n.tr('Configuration is OK'), LEVEL_DETAIL);
+ }
+ return rc;
+ }
+
+ /// Checks the needed access rights. If missing we request that.
+ /// Returns true: all rights are available
+ Future<bool> checkPermissions() async {
+ var rc = true;
+ //logger.log(i18n.tr('We do not check access rights'));
+ if (Platform.isAndroid) {
+ for (var permission in neededPermissions) {
+ logger.log(nameOfPermission(permission), LEVEL_FINE);
+ var status = await permission.status;
+ if (status == PermissionStatus.granted) {
+ logger.log('${nameOfPermission(permission)}: ${nameOfStatus(status)}',
+ LEVEL_DETAIL);
+ } else {
+ logger.log(i18n.tr('right') +
+ ' ${nameOfPermission(permission)}: ${nameOfStatus(status)}');
+ await permission.request();
+ status = await permission.status;
+ if (status != PermissionStatus.granted) {
+ logger.error(i18n.trArgs('missing right %1: %2', M,
+ [nameOfPermission(permission), nameOfStatus(status)]));
+ rc = _success = false;
+ }
+ }
+ }
+ }
+ if (rc) {
+ logger.log(i18n.tr('access rights OK'), LEVEL_DETAIL);
+ }
+ return rc;
+ }
+
+ /// Checks whether a valid configuration exists.
+ /// Returns true: a valid configuration exists.
+ bool hasValidConfiguration() {
+ final fileContent = configurationFile.readAsStringSync();
+ var rc = true;
+ try {
+ final map = convert.jsonDecode(fileContent);
+ final config = BaseConfiguration(map, logger);
+ final section = enumToString(serverEnvironment);
+ if (config.asString('host', section: section) == null ||
+ config.asInt('port', section: section) == null) {
+ rc = logger
+ .error('${configurationFile.path}: ' + i18n.tr('wrong: host/port'));
+ } else if (config.asString('configDirectory') == null ||
+ config.asString('dataDirectory') == null) {
+ rc = logger.error('${configurationFile.path}: ' +
+ i18n.tr('wrong: configDirectory/dataDirectory'));
+ }
+ } on FormatException catch (exc) {
+ rc = logger
+ .error('${configurationFile.path}: ' + i18n.tr('failure:' + ' $exc'));
+ }
+ return rc;
+ }
+
+ /// Prüft, ob eine gültige Datei mit den Bienenstockdaten existiert.
+ /// Liefert true, wenn ja.
+ bool hasValidHives() {
+ final fileContent = hivesFile.readAsStringSync();
+ final filename = hivesFile.path;
+ var rc = false;
+ try {
+ final list = convert.jsonDecode(fileContent);
+ rc = list is Iterable;
+ if (!rc) {
+ logger.error('$filename: "hives" ist kein Array');
+ } else {
+ var ix = -1;
+ for (var item in list) {
+ ix++;
+ rc = item is Map;
+ if (!rc) {
+ logger.error('$filename: hives[$ix] ist keine Map');
+ } else {
+ final map2 = item;
+ rc = (map2.containsKey('hiveid') &&
+ map2.containsKey('name') &&
+ map2.containsKey('lat') &&
+ map2.containsKey('long'));
+ if (!rc) {
+ logger.error('$filename: Fehler in hives[$ix]');
+ break;
+ }
+ }
+ }
+ }
+ } on FormatException catch (exc) {
+ logger.error('$filename: kein JSon: $exc');
+ }
+ return rc;
+ }
+
+ /// Liefert den Namen des Rechts [permission].
+ static String nameOfPermission(Permission permission) {
+ var rc;
+ if (permission == Permission.storage) {
+ rc = 'Speicher';
+ } else if (permission == Permission.camera) {
+ rc = 'Kamera';
+ } else {
+ if (permission == Permission.location) {
+ rc = 'Geo-Position';
+ } else {
+ rc = 'Permission-${permission.value}';
+ }
+ }
+ return rc;
+ }
+
+ /// Liefert den Namen des [status].
+ static String nameOfStatus(PermissionStatus status) {
+ String rc;
+ switch (status) {
+ case PermissionStatus.granted:
+ rc = 'erlaubt';
+ break;
+ case PermissionStatus.denied:
+ rc = 'verweigert';
+ break;
+ case PermissionStatus.restricted:
+ rc = 'eingeschränkt';
+ break;
+ case PermissionStatus.limited:
+ rc = 'begrenzt';
+ break;
+ case PermissionStatus.permanentlyDenied:
+ rc = 'gesperrt';
+ break;
+ }
+ return rc;
+ }
+
+ /// Initialisiert die einzige Instanz (Singleton).
+ static Future initialize(BaseLogger logger) async {
+ instance = Installation.internal(logger);
+ }
+}
flutter_markdown: ^0.6.1
flutter_bloc: ^7.1.0
equatable: ^2.0.3
+ permission_handler: ^7.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
import 'package:dart_bones/dart_bones.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
-import 'sql_storage.dart';
import 'package:mysql1/mysql1.dart';
+import 'sql_storage.dart';
const forbidden = 'forbidden';
const wrongData = 'wrong data';
const wrongParameters = 'wrong parameters';
/// That is a HTTP server serving only the method POST (for queries and storages).
/// The server is multi threaded by isolates.
class RestServer {
- static const _version = 'V2021.06.29.00';
+ static const _version = 'V2021.08.19.00';
static BaseLogger? unittestLogger;
BaseLogger logger = MemoryLogger(LEVEL_FINE);
BaseConfiguration configuration = BaseConfiguration({}, globalLogger);
address: 0.0.0.0
port: 58021
dataDirectory: /var/cache/exhibition/data
- sqlDirectory: /usr/share/exhibition/rest_server/data/sql
+ sqlDirectory: /usr/share/exhibition/data/sql
threads: 2
watchDogPause: 60
# logFile: /var/log/local/exhibition.log
answerLength: 200
db:
db: appexhibition
- user: jonny
+ user: exhibition
code: "Top Secret"
host: localhost
port: 3306
service.uninstallService(appName, user: appName, group: appName);
}
+ /// Returns the program version.
static String version() => _version;
+ static Future<String> sql(List<String> args, ArgResults results) async {
+ final logger = MemoryLogger(LEVEL_FINE);
+ final rc = StringBuffer();
+ var filename = results['configuration'];
+ if (!File(filename).existsSync()) {
+ print('+++ missing $filename');
+ unittestLogger?.error('missing configuration: $filename');
+ } else {
+ final configuration = Configuration.fromFile(filename, logger);
+ MySqlDb db = MySqlDb.fromConfiguration(configuration, logger);
+ await db.connect();
+ if (db.hasConnection){
+ for (var filename in args){
+ logger.log('=== script $filename:', LEVEL_DETAIL);
+ rc.writeln(await db.executeScriptFile(filename));
+ }
+ }
+ }
+ return rc.toString();
+ }
}
/// Implements an isolate instance.
buffer.write('[');
String? separator;
for (var item in list) {
- if (separator == null){
+ if (separator == null) {
separator = ',';
} else {
buffer.write(separator);
} else if (item is Map) {
rowToJson(item, buffer);
} else if (item is Iterable) {
- arrayToJson(item, buffer);
+ arrayToJson(item, buffer);
}
}
buffer.write(']');
}
break;
case SqlStatementType.update:
- final count =
- await db!.updateRaw(sql, params: positionalParameters);
+ final count = await db!.updateRaw(sql, params: positionalParameters);
rc = 'rows:$count';
break;
default:
if (results['help']) {
try {
try {
- final serviceName = 'exhibition-sv';
+ final serviceName = 'exhibition.sv';
print('''Usage $serviceName [mode] [options]
Starts a REST server answering the client database requests.
<mode>:
Uninstalls the program.
version
Displays the version.
+ sql <script1> [<script2> ...]
+ Executes a SQL script
<option>:
${parser.usage}
Examples:
$serviceName --help
$serviceName --example
$serviceName
-$serviceName --configuration=/tmp/exhibition.yaml
+$serviceName daemon --configuration=/tmp/exhibition.yaml
$serviceName -c /tmp/exhibition.yaml
$serviceName version
+$serviceName sql roles.sql users.sql
''');
} catch (e, s) {
print(s);
case 'version':
print(RestServer.version());
break;
+ case 'sql':
+ final data = await RestServer.sql(pureArguments, results);
+ print(data);
+ break;
default:
print('Unknown <mode>: $command. Try "--help"');
break;
args: ^2.1.0
path: ^1.8.0
yaml: ^3.1.0
- dart_bones: ^1.2.1
+ dart_bones: ^1.2.2
dev_dependencies:
lints: ^1.0.0
--- /dev/null
+import 'dart:io';
+import 'package:dart_bones/dart_bones.dart';
+import 'package:rest_server/services.dart';
+import 'package:test/test.dart';
+import 'package:path/path.dart' as path;
+
+void main() async {
+ final logger = MemoryLogger(LEVEL_DETAIL);
+ FileSync.initialize(logger);
+ final fileSync = FileSync();
+ final configFile = init(logger);
+ final baseDir = fileSync.tempDirectory('services', subDirs: 'unittest');
+ final configuration = Configuration.fromFile(configFile, logger);
+ //final fullTest = 'Never'.contains('x');
+ await TestDbInitializer.initTest(configuration, logger);
+ group('services', () {
+ test('version', () async {
+ await run(['version']);
+ expect(logger.errors.isEmpty, isTrue);
+ });
+ test('sql', () async {
+ final script = path.join(baseDir, 'script.sql');
+ fileSync.toFile(script, '''-- ! say === persons:
+select * from persons limit 1;
+''');
+ await run(['--configuration=$configFile', 'sql', script]);
+ expect(logger.errors.isEmpty, isTrue);
+ });
+ });
+}
+String init(BaseLogger logger) {
+ final fileSync = FileSync();
+ var rc = path.join(fileSync.tempDirectory('unittest'), 'config.yaml');
+ final sqlFile = '/tmp/unittest/sql/persons.sql.yaml';
+ fileSync.toFile(rc, '''---
+# Example configuration file for the rest server. Created by rest_server.
+service:
+ address: 0.0.0.0
+ port: 58031
+ dataDirectory: /tmp/unittest/data
+ sqlDirectory: ${path.dirname(sqlFile)}
+ threads: 1
+ watchDogPause: 60
+ # logFile: /var/log/local/exhibition.log
+trace:
+ answerLength: 200
+db:
+ db: dbtest
+ user: dbtest
+ code: "TopSecret"
+ host: localhost
+ port: 3306
+ primaryTable: persons
+ timeout: 30
+ traceDataLength: 200
+clientSessionTimeout: 900
+''');
+ fileSync.ensureDirectory('/tmp/unittest/sql');
+ fileSync.ensureDirectory('/tmp/unittest/data');
+ final file = File(sqlFile);
+ file.writeAsStringSync('''---
+# SQL statements of the module "Persons":
+module: Persons
+list:
+ type: list
+ parameters: []
+ sql: select * from persons;
+byId:
+ type: record
+ parameters: [ ":id" ]
+ sql: "select * from persons where person_id=:id;"
+update:
+ type: update
+ parameters: [":id", ":name", ":email", ":changedby"]
+ sql: "UPDATE persons SET
+ person_name=:name, person_email=:email, person_changed=NOW(), person_changedby=:changedby
+ WHERE person_id=:id;"
+insert:
+ type: insert
+ parameters: [":name", ":email", ":createdby"]
+ sql: "INSERT INTO persons(person_name, person_email, person_created, person_createdby)
+ VALUES(:name, :email, NOW(), :createdby);"
+delete:
+ type: delete
+ parameters: [':id']
+ sql: "DELETE FROM persons WHERE person_id=:id;"
+''');
+ return rc;
+}
+
+
+class TestDbInitializer {
+ MySqlDb? db;
+ final BaseConfiguration configuration;
+ final BaseLogger logger;
+ List<dynamic>? tables;
+ TestDbInitializer(this.configuration, this.logger) {
+ db = MySqlDb.fromConfiguration(configuration, logger);
+ db!.timeout = 30;
+ }
+
+ Future init() async {
+ if (!(db?.hasConnection ?? false)) {
+ await db!.connect();
+ }
+ if (!(db?.hasConnection ?? false)) {
+ logger.error('cannot connect to database');
+ } else {
+ tables = await db!.readAllAsLists('show tables;');
+ }
+ }
+
+ Future initPersons(MySqlDb db, BaseLogger logger) async {
+ String sql;
+ if (!await db.hasTable('persons')) {
+ sql = '''CREATE TABLE persons(
+ person_id int(10) unsigned NOT NULL AUTO_INCREMENT,
+ person_name varchar(200) NOT NULL,
+ person_email varchar(200) NOT NULL,
+ person_created timestamp NULL,
+ person_createdby varchar(16),
+ person_changed timestamp NULL,
+ person_changedby varchar(16),
+ PRIMARY KEY (person_id)
+);
+''';
+ if (await db.execute(sql) < 0) {
+ logger.error('cannot create persons');
+ }
+ }
+ final count = await db.readOneInt('select count(*) from persons;');
+ if (count == null || count < 3) {
+ sql = '''insert into persons
+(person_id, person_name, person_email, person_created, person_createdby) values
+(11, 'Jones', 'jones@hamatoma.de', NOW(), 'daemon'),
+(12, 'Miller', 'miller@hamatoma.de', NOW(), 'daemon'),
+(13, 'Smith', 'smith@hamatoma.de', NOW(), 'daemon');
+''';
+ if (await db.execute(sql) < 0) {
+ logger.error('cannot create persons records');
+ }
+ }
+ }
+
+ Future initDbData() async {
+ try {
+ await init();
+ await initPersons(db!, logger);
+ } finally {
+ db?.close();
+ }
+ }
+
+ static Future initTest(
+ BaseConfiguration configuration, BaseLogger logger) async {
+ final initializer = TestDbInitializer(configuration, logger);
+ await initializer.initDbData();
+ initializer.db?.close();
+ }
+}
final fileSync = FileSync();
final baseDir = init(logger);
final targetDir = join(baseDir, nodeTarget);
+ fileSync.ensureDirectory(targetDir);
test('parse', () {
final parser = I18nTextParser(logger);
parser.scanDirectory(baseDir, 0);
final baseDir = fileSync.tempDirectory('i18n', subDirs: 'unittest');
final subDir = fileSync.tempDirectory('base', subDirs: 'unittest/i18n/lib');
fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io';
-final i18N = I18N();
-final M = i18N.module('Example');
+final i18n = I18N();
+final M = i18n.module('Example');
String header(){
- return i18N.tr('introduction', M) + " " + i18N.tr(
+ return i18n.tr('introduction', M) + " " + i18n.tr(
"description",
M);
}
-String count() => i18N.ntr('one piece', '%d pieces', "Statistic", 3);
+String count() => i18n.ntr('one piece', '%d pieces', "Statistic", 3);
String footer(){
return I18N().tr("Status line");
}
''');
fileSync.toFile(join(subDir, 'args.dart'), r'''import 'dart:io';
-final i18N = I18N();
-final M = i18N.module("Sample");
-String count() => i18N.ntr(
+final i18n = I18N();
+final M = i18n.module("Sample");
+String count() => i18n.ntr(
'one piece',
'%d pieces', M, countItems('any'));
String header(String user, String role){
- return i18N.trArgs('Name: {0} Role: {1}', M, [user, role]) + " " + i18N.tr(
+ return i18n.trArgs('Name: {0} Role: {1}', M, [user, role]) + " " + i18n.tr(
r'info', M);
}
String footer(){
final subDir =
fileSync.tempDirectory('base', subDirs: 'unittest/i18n_parser/lib');
fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io';
-final i18N = I18N();
-final M = i18N.module('Example');
+final i18n = I18N();
+final M = i18n.module('Example');
String header(){
- return i18N.tr('introduction', M) + " " + i18N.tr(
+ return i18n.tr('introduction', M) + " " + i18n.tr(
"description",
M);
}
-String count() => i18N.ntr('one piece', '%d pieces', "Statistic", 3);
+String count() => i18n.ntr('one piece', '%d pieces', "Statistic", 3);
String footer(){
return I18N().tr("Status line");
}
''');
fileSync.toFile(join(subDir, 'args.dart'), r'''import 'dart:io';
-final i18N = I18N();
-final M = i18N.module("Sample");
-String count() => i18N.ntr(
+final i18n = I18N();
+final M = i18n.module("Sample");
+String count() => i18n.ntr(
'one piece',
'%d pieces', M, countItems('any'));
String header(String user, String role){
- return i18N.trArgs('Name: {0} Role: {1}', M, [user, role]) + " " + i18N.tr(
+ return i18n.trArgs('Name: {0} Role: {1}', M, [user, role]) + " " + i18n.tr(
r'info', M);
}
String footer(){
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
+import 'package:exhibition/exhibition_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
+ await tester.pumpWidget(ExhibitionApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
#! /bin/bash
APP=$1
APP2=$(echo ${APP:0:1} | tr a-z A-Z)${APP:1}
+DO_UPDATE=$2
function Usage(){
- echo "Usage: InitProject PROJECT"
+ echo "Usage: InitProject PROJECT [--update]"
echo " Prepares the project PROJECT for using the framework exhibition"
echo "PROJECT: the name of the already existing project (created with 'flutter create')"
echo "Example:"
local projDir=$(pwd)
cd ../exhibition
local srcDir=$(pwd)
- for file in lib/base/*.dart lib/meta/module_meta_data.dart lib/meta/modules.dart \
- rest_server/pubspec.yaml \
- tools/CompileMerge tools/PackRestServer \
- ; do
+ for file in lib/base/*.dart \
+ lib/meta/module_meta_data.dart \
+ lib/meta/modules.dart \
+ rest_server/pubspec.yaml \
+ tools/PackRestServer \
+ ; do
cd $projDir
- local dir=$(dirname $file)
- EnsureDir $dir
- echo "creating $file"
- cp -a ../exhibition/$file $file
+ if [ -z "$DO_UPDATE" -o ! -e $file ]; then
+ local dir=$(dirname $file)
+ EnsureDir $dir
+ echo "creating $file"
+ cp -a ../exhibition/$file $file
+ fi
cd $srcDir
done
cd $projDir
}
+function CopyFile(){
+ local projDir=$1
+ local file=$2
+ local srcDir=$3
+ cd $projDir
+ local dir=$(dirname $file)
+ EnsureDir $dir
+ local file2=${file/exhibition/$APP}
+ echo "copy and replace $file -> $(basename $file2)"
+ sed <../exhibition/$file >$file2 -e "s/exhibition/$APP/g" -e "s/Exhibition/$APP2/g"
+ chmod --reference=../exhibition/$file $file2
+ cd $srcDir
+
+}
function CopyAndReplace(){
local projDir=$(pwd)
cd ../exhibition
local srcDir=$(pwd)
- for file in lib/setting/*.dart \
- pubspec.yaml \
+ local filesNewOnly=$(echo pubspec.yaml \
lib/persistence/*.dart \
lib/page/*.dart \
- rest_server/lib/*.dart rest_server/bin/*.dart rest_server/tools/project.inc \
- rest_server/CR \
lib/main.dart \
lib/meta/*_meta.dart \
bin/*.dart \
- ; do
- cd $projDir
- local dir=$(dirname $file)
- EnsureDir $dir
- local file2=${file/exhibition/$APP}
- echo "copy and replace $file -> $(basename $file2)"
- sed <../exhibition/$file >$file2 -e "s/exhibition/$APP/g" -e "s/Exhibition/$APP2/g"
- chmod --reference=../exhibition/$file $file2
- cd $srcDir
- done
+ )
+ local filesAlways=$(echo rest_server/lib/*.dart \
+ rest_server/bin/*.dart \
+ rest_server/tools/project.inc \
+ rest_server/CR
+ )
+
+ if [ -z "$DO_UPDATE" ]; then
+ for file in lib/setting/*.dart \
+ pubspec.yaml \
+ lib/persistence/*.dart \
+ lib/page/*.dart \
+ rest_server/lib/*.dart rest_server/bin/*.dart rest_server/tools/project.inc \
+ rest_server/CR \
+ lib/main.dart \
+ lib/meta/*_meta.dart \
+ bin/*.dart \
+ ; do
+ CopyFile $projDir $file $srcDir
+ done
+ else
+ fi
cd $projDir
}
function SymbolicLinks(){
- for links in ReCreateMetaTool:. Meta:. ; do
+ for links in ReCreateMetaTool:. Meta:. dart_tools:. \
+ ; do
local src=../exhibition/${links%:*}
local trg=${links#*:}
local trg2=$trg
ln -s $src $trg2
fi
done
+ for links in dart_tools/tools/i18n_text_parser:tools \
+ dart_tools/tools/yaml_merger:tools \
+ ; do
+ local src=../../exhibition/${links%:*}
+ local trg=${links#*:}
+ local trg2=$trg
+ test $trg = . && trg2=$(basename $src)
+ echo "trg: $trg2"
+ if [ ! -L $trg2 ]; then
+ echo "linking $src -> $trg2"
+ ln -s $src $trg2
+ fi
+ done
}
function PrepareTools(){
- test -e tools/yaml_merger || tools/CompileMerger
+ local tool
+ local curDir=$(pwd)
+ echo "PrepareTools: pwd=$curDir"
+ for tool in i18n_text_parser:CompileI18n \
+ yaml_merger:CompileMerge; do
+ local exe=${tool%:*}
+ local script=${tool#*:}
+ if [ ! -e ../exhibition/dart_tools/tools/$exe ]; then
+ cd ../exhibition/dart_tools
+ echo "executing $script"
+ tools/$script
+ fi
+ cd $curDir
+ done
+}
+function Flutter(){
+ local app=$1
+ flutter create $app
+ cd $app
+ flutter config linux
+}
+function Finish(){
+ pub upgrade
}
if [ -z "$APP" ]; then
Usage "missing PROJECT"
-elif [ ! -d ../$APP ]; then
+elif [ -n $DO_UPDATE -a $DO_UPDATE != '--update' ]; then
+ Usage "Unknown option: $DO_UPDATE"
+elif [ -d $APP ]; then
+ Usage "PROJECT already exists."
+elif [ ! -d exhibition/dart_tools ]; then
Usage "wrong current directory: Please go into the base folder of the project."
else
+ test -n "$DO_UPDATE" && Flutter $APP
PrepareTools
MkDirs
CopyFiles
CopyAndReplace
SymbolicLinks
+ Finish
fi
TAR_NODE=rest_server.tgz
TAR=/tmp/$TAR_NODE
source rest_server/tools/project.inc
+EXE=$PROJECT.sv
function DoIt(){
test -d $BASE_DIR && rm -Rf $BASE_DIR
- mkdir -p $BASE_DIR/data/sql
+ mkdir -p $BASE_DIR/data/yaml_merger
cd rest_server
./CR
test -d data/sql/precedence || mkdir -p data/sql/precedence
cd ..
- cp -av rest_server/tools/rest_server $BASE_DIR
- tools/CompileMerge
- tools/yaml_merge data/sql data/sql/precedence $BASE_DIR/data/sql
+ cp -av rest_server/tools/rest_server $BASE_DIR/$EXE
+ tools/yaml_merger rest_server/data/sql rest_server/data/sql/precedence $BASE_DIR/data/sql
cat <<EOS >$BASE_DIR/INSTALL.TXT
# ------------
# Installation:
DIR=\$(pwd)
mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT
tar xzf \DIR/$TAR_NODE
-# ./rest_server install <executable> <service-name>
-./rest_server install /usr/share/$PROJECT/rest_server $PROJECT
+# ./$EXE install <executable> <service-name>
+./$EXE install /usr/share/$PROJECT/$EXE $PROJECT
# see /usr/share/$PROJECT and /etc/$PROJECT
EOS
tar -C $BASE_DIR -czf $TAR .
DIR=\$(pwd)
mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT
tar xzf \$DIR/rest_server.tgz
-./rest_server install /usr/share/$PROJECT/rest_server $PROJECT
+./$EXE install /usr/share/$PROJECT/$EXE $PROJECT
+sed -i -e "s/WorkingDirectory=.*/WorkingDirectory=\/usr\/share\/$PROJECT/" \
+ /etc/systemd/system/$PROJECT.service
+cd /etc/$PROJECT
+test -e $PROJECT.yaml && ln -s $PROJECT.yaml rest_server.yaml
EOS
chmod uog+x $SCRIPT_INSTALL
echo "================"