Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0fc64403 | ||
|
|
dae521ae89 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
!/bin/.*.ts
|
||||||
|
!/tools/.*.ts
|
||||||
+5
-15
@@ -8,29 +8,19 @@
|
|||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": [
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-shadow": ["error", { "allow": ["err"] }],
|
"no-shadow": ["error", { "allow": ["err"] }],
|
||||||
"handle-callback-err": "error",
|
"handle-callback-err": "error",
|
||||||
"prefer-arrow-callback": "error",
|
"prefer-arrow-callback": "error",
|
||||||
"curly": ["error", "multi-or-nest", "consistent"],
|
"curly": ["error", "multi-or-nest", "consistent"],
|
||||||
"no-buffer-constructor": "error",
|
"no-buffer-constructor": "error",
|
||||||
"prefer-const": ["error", { "destructuring": "all" }],
|
"prefer-const": ["error", {"destructuring": "all"}],
|
||||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
"eqeqeq": ["error", "always", {"null": "ignore"}],
|
||||||
"no-return-await": "error",
|
"no-return-await": "error",
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
"@typescript-eslint/no-use-before-define": [
|
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
|
||||||
"error",
|
"@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }],
|
||||||
{ "functions": false }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": [
|
|
||||||
"error",
|
|
||||||
{ "allowExpressions": true }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
"require-atomic-updates": 0,
|
"require-atomic-updates": 0,
|
||||||
"no-prototype-builtins": 0
|
"no-prototype-builtins": 0
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
*~
|
*~
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
bin/.*.ts
|
||||||
|
tools/.*.ts
|
||||||
docs/_build
|
docs/_build
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
*~
|
||||||
|
.npmignore
|
||||||
|
|
||||||
|
config/config.json
|
||||||
|
config/cwmp.key
|
||||||
|
config/cwmp.crt
|
||||||
|
config/cwmp.ca-bundle
|
||||||
|
config/nbi.key
|
||||||
|
config/nbi.crt
|
||||||
|
config/nbi.ca-bundle
|
||||||
|
config/fs.key
|
||||||
|
config/fs.crt
|
||||||
|
config/fs.ca-bundle
|
||||||
|
config/ext
|
||||||
|
|
||||||
|
debug/*
|
||||||
|
!debug/.gitkeep
|
||||||
|
|
||||||
|
dist
|
||||||
|
.eslintrc.json
|
||||||
|
.eslintignore
|
||||||
|
ava.config.js
|
||||||
|
tsconfig.json
|
||||||
|
bin/.*
|
||||||
|
tools/.*
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
dist
|
|
||||||
npm-shrinkwrap.json
|
|
||||||
+72
-113
@@ -1,60 +1,20 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 1.2.5 (2021-03-12)
|
|
||||||
|
|
||||||
- Support specifying custom types when uploading files.
|
|
||||||
- Fix JS compatibility issue with Safari browser.
|
|
||||||
|
|
||||||
## 1.2.4 (2021-02-24)
|
|
||||||
|
|
||||||
- The data model state of a CPE is no longer forgotten after unsuccessful
|
|
||||||
session termination (e.g. timeout). This addresses a number of undesired side
|
|
||||||
effects that arise when a CPE does not terminate the session properly.
|
|
||||||
- Executing tasks that take a long time to complete (e.g. refreshing the entire
|
|
||||||
data model) no longer shows a timeout error while the task is still being
|
|
||||||
processed.
|
|
||||||
- New function `ROUND()` available to expressions. It works similar to the
|
|
||||||
function by the same name in SQLite and PostgreSQL.
|
|
||||||
- Log access events for `genieacs-nbi` service.
|
|
||||||
- Pipe stdout/stderr from extension scripts to the `genieacs-cwmp` process log.
|
|
||||||
- Parameter values of type `xsd:dateTime` are now displayed in the UI and CSV
|
|
||||||
downloads as a date string rather than a numeric value.
|
|
||||||
- Add file download link in the files listing page.
|
|
||||||
- Display spinner loading animation throughout the UI.
|
|
||||||
- Display GenieACS version and build number underneath the logo.
|
|
||||||
- New option to specify how many parameters are displayed at a time in the
|
|
||||||
all-parameters component. Simply set `limit` property in the component config.
|
|
||||||
- Reduce overly strong Brotli compression level which was causing significant
|
|
||||||
page load slowdown when Brotli is used.
|
|
||||||
- Retire dump-data-model tool. `genieacs-sim` can now use a CSV file as its data
|
|
||||||
model.
|
|
||||||
- Reduce the number of concurrent database connections from each process.
|
|
||||||
- Remove dependency on 'hostInfo' MongoDB command which is a privileged action.
|
|
||||||
It is now possible to use shared MongoDB instances with limited privileged.
|
|
||||||
- Fix bug in NBI where querying files returns 404 error.
|
|
||||||
- Fix ping not working for devices with an IPv6 address.
|
|
||||||
- Fix an elusive memory leak in `genieacs-fs` that slowly eats up memory and can
|
|
||||||
go unnoticed for long periods of time.
|
|
||||||
- Fix a rare edge case where a `declare()` call to set a parameter value may not
|
|
||||||
work as intended if the parameter was originally received as part of the
|
|
||||||
Inform message.
|
|
||||||
- A number of other fixes and stability improvements.
|
|
||||||
|
|
||||||
## 1.2.3 (2020-10-26)
|
## 1.2.3 (2020-10-26)
|
||||||
|
|
||||||
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
|
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
|
||||||
report writable parameters as non-writable. When set to true, the scripts will
|
report writable parameters as non-writable. When set to true, the scripts will
|
||||||
no longer respect the 'writable' attribute of the CPE parameters and will send
|
no longer respect the 'writable' attribute of the CPE parameters and will send
|
||||||
a SetParamteerValues, AddObject, or DeleteObject request anyway.
|
a SetParamteerValues, AddObject, or DeleteObject request anyway.
|
||||||
- Tags no longer restrict what characters are allowed. Any character other than
|
- Tags no longer restrict what characters are allowed. Any character other than
|
||||||
alphanumeric characters, hyphen, or underscore is now encoded in the data
|
alphanumeric characters, hyphen, or underscore is now encoded in the data
|
||||||
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
|
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
|
||||||
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
|
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
|
||||||
- Better XML validation to avoid crashes caused by invalid CPE requests.
|
- Better XML validation to avoid crashes caused by invalid CPE requests.
|
||||||
- Fix confusing 404 error message when the user attempts to modify a resource
|
- Fix confusing 404 error message when the user attempts to modify a resource
|
||||||
when they don't have the necessary permissions.
|
when they don't have the necessary permissions.
|
||||||
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
|
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
|
||||||
running for a few weeks.
|
running for a few weeks.
|
||||||
- Fix exception when IS NULL operator is used in certain situations.
|
- Fix exception when IS NULL operator is used in certain situations.
|
||||||
|
|
||||||
## 1.2.2 (2020-10-03)
|
## 1.2.2 (2020-10-03)
|
||||||
@@ -64,80 +24,81 @@
|
|||||||
- Fix exception that can happen and persist after a Download request.
|
- Fix exception that can happen and persist after a Download request.
|
||||||
- Fix validation bug preventing running refreshObject task on data model root.
|
- Fix validation bug preventing running refreshObject task on data model root.
|
||||||
- Fix invalid arguments fault in refresh preset configuration when upgrading
|
- Fix invalid arguments fault in refresh preset configuration when upgrading
|
||||||
from v1.1.
|
from v1.1.
|
||||||
|
|
||||||
## 1.2.1 (2020-09-08)
|
## 1.2.1 (2020-09-08)
|
||||||
|
|
||||||
- Fix bug causing faults to not be displayed in the UI.
|
- Fix bug causing faults to not be displayed in the UI.
|
||||||
- Fix bug where deleting objects does not get reflected immediately in the UI.
|
- Fix bug where deleting objects does not get reflected immediately in the UI.
|
||||||
- Improve conversion between filters written in the expression format and
|
- Improve conversion between filters written in the expression format and
|
||||||
MongoDB queries. There should now be fewer edge cases where the two are not
|
MongoDB queries. There should now be fewer edge cases where the two are not
|
||||||
equisatisfiable.
|
equisatisfiable.
|
||||||
|
|
||||||
## 1.2.0 (2020-09-01)
|
## 1.2.0 (2020-09-01)
|
||||||
|
|
||||||
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
|
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
|
||||||
- Support CASE statement and COALESCE function in expressions.
|
- Support CASE statement and COALESCE function in expressions.
|
||||||
- Provision arguments can now be a list of expressions that are dynamically
|
- Provision arguments can now be a list of expressions that are dynamically
|
||||||
evaluated.
|
evaluated.
|
||||||
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
|
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
|
||||||
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
|
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
|
||||||
- Config expressions can now access all available device parameters, not only
|
- Config expressions can now access all available device parameters, not only
|
||||||
serial number, product class, and OUI.
|
serial number, product class, and OUI.
|
||||||
- Use relative URLs throughout the UI to allow serving from a subdirectory using
|
- Use relative URLs throughout the UI to allow serving from a subdirectory
|
||||||
a reverse proxy.
|
using a reverse proxy.
|
||||||
- Make Date.parse() and Date.UTC() available to provision scripts.
|
- Make Date.parse() and Date.UTC() available to provision scripts.
|
||||||
- libxmljs has been entirely removed in favor of our bespoke XML parser.
|
- libxmljs has been entirely removed in favor of our bespoke XML parser.
|
||||||
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now used
|
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now
|
||||||
to determine the TCP connection timeout.
|
used to determine the TCP connection timeout.
|
||||||
- The all-parameters component now limits the number of parameters displayed for
|
- The all-parameters component now limits the number of parameters displayed
|
||||||
better performance.
|
for better performance.
|
||||||
- The process genieacs-cwmp is now much less likely to throw exceptions as a
|
- The process genieacs-cwmp is now much less likely to throw exceptions as a
|
||||||
result of invalid requests from CPE.
|
result of invalid requests from CPE.
|
||||||
- A large number of bug fixes and stability improvements.
|
- A large number of bug fixes and stability improvements.
|
||||||
|
|
||||||
## 1.2.0-beta.0 (2019-07-30)
|
## 1.2.0-beta.0 (2019-07-30)
|
||||||
|
|
||||||
- A brand new UI superseding genieacs-gui.
|
- A brand new UI superseding genieacs-gui.
|
||||||
- New initialization wizard on first run.
|
- New initialization wizard on first run.
|
||||||
- New expression/query language used in search filters and preset preconditions.
|
- New expression/query language used in search filters and preset
|
||||||
|
preconditions.
|
||||||
- CPE -> ACS authentication is now supported.
|
- CPE -> ACS authentication is now supported.
|
||||||
- New config option (CWMP_KEEP_ALIVE_TIMEOUT) to specify how long to wait for a
|
- New config option (CWMP_KEEP_ALIVE_TIMEOUT) to specify how long to wait for a
|
||||||
reply from the CPE before closing the TCP connection.
|
reply from the CPE before closing the TCP connection.
|
||||||
- Debug logging has been reimplemented utilizing YAML format for logs.
|
- Debug logging has been reimplemented utilizing YAML format for logs.
|
||||||
- Handle 9005 faults (Invalid Parameter Name) gracefully by attempting to
|
- Handle 9005 faults (Invalid Parameter Name) gracefully by attempting to
|
||||||
rediscover the path of the missing parameter recursively.
|
rediscover the path of the missing parameter recursively.
|
||||||
- declare() statements not followed by an explicit commit() are now deferred
|
- declare() statements not followed by an explicit commit() are now deferred
|
||||||
until all currently active scripts have been executed.
|
until all currently active scripts have been executed.
|
||||||
- FS_HOSTNAME now defaults to the server's hostname or IP.
|
- FS_HOSTNAME now defaults to the server's hostname or IP.
|
||||||
- The API now validates the structure of task objects before saving.
|
- The API now validates the structure of task objects before saving.
|
||||||
- New XML parser implementation for better performance. You can revert to the
|
- New XML parser implementation for better performance. You can revert to the
|
||||||
old parser by enabling the config option XML_LIBXMLJS. Requires Node.js v11 or
|
old parser by enabling the config option XML_LIBXMLJS. Requires Node.js v11 or
|
||||||
v10.
|
v10.
|
||||||
- Performance optimizations. While performance has improved for the majority of
|
- Performance optimizations. While performance has improved for the majority of
|
||||||
use cases, there may be situations where performance has degraded. It's
|
use cases, there may be situations where performance has degraded. It's
|
||||||
recommended to revisit your hardware requirements.
|
recommended to revisit your hardware requirements.
|
||||||
- Connection request authentication no longer uses 'auth.js' file. Instead, the
|
- Connection request authentication no longer uses 'auth.js' file. Instead, the
|
||||||
connection request authentication behavior can now be customized using an
|
connection request authentication behavior can now be customized using an
|
||||||
'expression'.
|
'expression'.
|
||||||
- The config file (config.json) has been deprecated. System configuration (e.g.
|
- The config file (config.json) has been deprecated. System configuration (e.g.
|
||||||
listen ports, worker count) are now recommended to be passed as environment.
|
listen ports, worker count) are now recommended to be passed as environment.
|
||||||
variables. Other general configuration options are stored in the database so
|
variables. Other general configuration options are stored in the database so as
|
||||||
as to not require service restart for changes to take effect.
|
to not require service restart for changes to take effect.
|
||||||
- Optional redis dependency has been removed completely.
|
- Optional redis dependency has been removed completely.
|
||||||
- Tags now allow only alphanumeric characters and underscore.
|
- Tags now allow only alphanumeric characters and underscore.
|
||||||
- Supported versions of Node.js and MongoDB are 10.x and up and 2.6 and up
|
- Supported versions of NodeJs and MongoDB are 10.x and up and 2.6 and up
|
||||||
respectively.
|
respectively.
|
||||||
|
|
||||||
## 1.1.3 (2018-10-23)
|
## 1.1.3 (2018-10-23)
|
||||||
|
|
||||||
- New config option (MAX_COMMIT_ITERATIONS) to avoid max commit iterations
|
- New config option (MAX_COMMIT_ITERATIONS) to avoid max commit iterations
|
||||||
faults for more complex scripts.
|
faults for more complex scripts.
|
||||||
- Support base64 and hexBinary parameter types.
|
- Support base64 and hexBinary parameter types.
|
||||||
- Strict parsing of number values in queries (e.g. "123abc" no longer accepted
|
- Strict parsing of number values in queries (e.g. "123abc" no longer accepted
|
||||||
as 123).
|
as 123).
|
||||||
- Mixing $ne and $not operators is not allowed. Now it throws an error instead
|
- Mixing $ne and $not operators is not allowed. Now it throws an error instead
|
||||||
of returning incorrect results.
|
of returning incorrect results.
|
||||||
- When a task expires, any associated fault is also deleted.
|
- When a task expires, any associated fault is also deleted.
|
||||||
- API now accepts 'timeout' argument when posting a task.
|
- API now accepts 'timeout' argument when posting a task.
|
||||||
- A number of stability fixes.
|
- A number of stability fixes.
|
||||||
@@ -147,68 +108,66 @@
|
|||||||
- A large number of bug fixes as well as stability and performance improvements.
|
- A large number of bug fixes as well as stability and performance improvements.
|
||||||
- Three security vulnerabilities disclosed by Maximilian Hils have been patched.
|
- Three security vulnerabilities disclosed by Maximilian Hils have been patched.
|
||||||
- New config option UDP_CONNECTION_REQUEST_PORT to specify binding port for UDP
|
- New config option UDP_CONNECTION_REQUEST_PORT to specify binding port for UDP
|
||||||
connection requests.
|
connection requests.
|
||||||
- New config option DATETIME_MILLISECONDS to strip milliseconds from dateTime
|
- New config option DATETIME_MILLISECONDS to strip milliseconds from dateTime
|
||||||
values.
|
values.
|
||||||
- New config option BOOLEAN_LITERAL to use 1/0 or true/false for boolean values.
|
- New config option BOOLEAN_LITERAL to use 1/0 or true/false for boolean values.
|
||||||
- Parameter values that cannot be parsed according to the reported type now show
|
- Parameter values that cannot be parsed according to the reported type now show
|
||||||
a warning message.
|
a warning message.
|
||||||
- Virtual parameter scripts now use the variable 'args' instead of the special
|
- Virtual parameter scripts now use the variable 'args' instead of the special
|
||||||
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is:
|
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is: {declare
|
||||||
{declare timestamps}, {declare values}, {current timestamps}, {current
|
timestamps}, {declare values}, {current timestamps}, {current values}.
|
||||||
values}.
|
|
||||||
- Virtual parameter value types are now inferred from the JavaScript type if the
|
- Virtual parameter value types are now inferred from the JavaScript type if the
|
||||||
returned value attribute is not a value-type pair.
|
returned value attribute is not a value-type pair.
|
||||||
- Show a fault when a virtual parameter script doesn't return the required
|
- Show a fault when a virtual parameter script doesn't return the required
|
||||||
attributes.
|
attributes.
|
||||||
- Redis is now optional (and disabled by default), reducing the complexity of
|
- Redis is now optional (and disabled by default), reducing the complexity of
|
||||||
scalable deployments.
|
scalable deployments.
|
||||||
- Better detection of cyclical presets resulting in fewer faults for complex
|
- Better detection of cyclical presets resulting in fewer faults for complex
|
||||||
provisioning scripts.
|
provisioning scripts.
|
||||||
- Math.random() is now deterministic on per-device basis. A function has been
|
- Math.random() is now deterministic on per-device basis. A function has been
|
||||||
added to allow specifying a seed value (e.g. Math.random.seed(Date.now())).
|
added to allow specifying a seed value (e.g. Math.random.seed(Date.now())).
|
||||||
- Overload spikes are now handled gracefully by refusing to accept new sessions
|
- Overload spikes are now handled gracefully by refusing to accept new sessions
|
||||||
temporarily when under abnormal load.
|
temporarily when under abnormal load.
|
||||||
- Added log messages for session timeouts, connection drops, and XML parsing
|
- Added log messages for session timeouts, connection drops, and XML parsing
|
||||||
errors.
|
errors.
|
||||||
- Date.now() now takes an optional argument to specify "time steps" (in
|
- Date.now() now takes an optional argument to specify "time steps" (in
|
||||||
milliseconds). This can be used to ensure a group of parameters are all
|
milliseconds). This can be used to ensure a group of parameters are all
|
||||||
refreshed at the same time intervals.
|
refreshed at the same time intervals.
|
||||||
- Only the non-default configuration options are now logged at process start.
|
- Only the non-default configuration options are now logged at process start.
|
||||||
- Faults caused by errors from extensions now show a cleaner stack trace.
|
- Faults caused by errors from extensions now show a cleaner stack trace.
|
||||||
- Exit main process if there are too many worker crashes (e.g. when DB is down).
|
- Exit main process if there are too many worker crashes (e.g. when DB is down).
|
||||||
- Updated dependencies and included a lockfile to ensure installations get the
|
- Updated dependencies and included a lockfile to ensure installations get the
|
||||||
exact dependencies it was tested against.
|
exact dependencies it was tested against.
|
||||||
|
|
||||||
## 1.1.1 (2017-03-23)
|
## 1.1.1 (2017-03-23)
|
||||||
|
|
||||||
- Avoid crashing when connection request credentials are missing.
|
- Avoid crashing when connection request credentials are missing.
|
||||||
- Show a warning instead of crashing when failing to parse parameter values
|
- Show a warning instead of crashing when failing to parse parameter values
|
||||||
according to the expected value type.
|
according to the expected value type.
|
||||||
- Add missing "Registered" event.
|
- Add missing "Registered" event.
|
||||||
- Fix bug where in certain cases many more instances than declared are created.
|
- Fix bug where in certain cases many more instances than declared are created.
|
||||||
- Fix parameter discovery bug when declared path timestamp is 1 or is not set.
|
- Fix parameter discovery bug when declared path timestamp is 1 or is not set.
|
||||||
- Fix preset precondition failing when testing against datetime parameters and
|
- Fix preset precondition failing when testing against datetime parameters and
|
||||||
certain other parameters like \_deviceId.\_ProductClass.
|
certain other parameters like \_deviceId.\_ProductClass.
|
||||||
|
|
||||||
## 1.1.0 (2017-03-10)
|
## 1.1.0 (2017-03-10)
|
||||||
|
|
||||||
- Provisions enable implementing dynamic device configuration or complex device
|
- Provisions enable implementing dynamic device configuration or complex device
|
||||||
provisioning work flow using arbitrary scripts.
|
provisioning work flow using arbitrary scripts.
|
||||||
- Virtual parameters are user-defined parameters whose values are evaluated from
|
- Virtual parameters are user-defined parameters whose values are evaluated from
|
||||||
a custom script.
|
a custom script.
|
||||||
- Extensions are sandboxed Node.js scripts that are accessible from provision
|
- Extensions are sandboxed Node.js scripts that are accessible from provision
|
||||||
and virtual parameter scripts to facilitate integration with external
|
and virtual parameter scripts to facilitate integration with external entities.
|
||||||
entities.
|
- Support for UDP/STUN based connection requests for reaching devices behind
|
||||||
- Support for UDP/STUN based connection requests for reaching devices behind NAT
|
NAT (TR-069 Annex G).
|
||||||
(TR-069 Annex G).
|
|
||||||
- Presets can now be scheduled using a cron-like expression.
|
- Presets can now be scheduled using a cron-like expression.
|
||||||
- Presets can now be tied to specific device events (e.g. boot).
|
- Presets can now be tied to specific device events (e.g. boot).
|
||||||
- Presets precondition queries no longer support "\$or" or other MongoDB logical
|
- Presets precondition queries no longer support "$or" or other MongoDB logical
|
||||||
operators.
|
operators.
|
||||||
- Faults are no longer a part of tasks but are now first class objects.
|
- Faults are no longer a part of tasks but are now first class objects.
|
||||||
- Presets are now assigned to channels. A fault in one channel only blocks
|
- Presets are now assigned to channels. A fault in one channel only blocks
|
||||||
presets in that channel.
|
presets in that channel.
|
||||||
- New API CRUD functions for provisions, virtual parameters, and faults.
|
- New API CRUD functions for provisions, virtual parameters, and faults.
|
||||||
- New config options for XML output.
|
- New config options for XML output.
|
||||||
- API responses now include "GenieACS-Version" header.
|
- API responses now include "GenieACS-Version" header.
|
||||||
@@ -217,19 +176,19 @@
|
|||||||
- Supported Node.js versions are 6.x and 7.x.
|
- Supported Node.js versions are 6.x and 7.x.
|
||||||
- Supported MongoDB versions are 2.6 through 3.4.
|
- Supported MongoDB versions are 2.6 through 3.4.
|
||||||
- Expect performance differences due to major under the hood changes. Some
|
- Expect performance differences due to major under the hood changes. Some
|
||||||
operations are faster and some are slower. Overall performance is improved.
|
operations are faster and some are slower. Overall performance is improved.
|
||||||
- GenieACS will no longer fetch the entire device data model upon first contact
|
- GenieACS will no longer fetch the entire device data model upon first contact
|
||||||
but will instead only fetch the parameters it needs to fulfill the presets.
|
but will instead only fetch the parameters it needs to fulfill the presets.
|
||||||
- Logs have been overhauled and split into two streams: process log (stderr) and
|
- Logs have been overhauled and split into two streams: process log (stderr) and
|
||||||
access log (stdout). Also added config options to dump logs to files rather
|
access log (stdout). Also added config options to dump logs to files rather than
|
||||||
than standard streams.
|
standard streams.
|
||||||
- Connection request authentication credentials are picked up from the device
|
- Connection request authentication credentials are picked up from the device
|
||||||
data model if available. config/auth.js is still supported as a fallback and
|
data model if available. config/auth.js is still supported as a fallback and now
|
||||||
now supports an optional callback argument.
|
supports an optional callback argument.
|
||||||
- Custom commands have been removed. Use virtual parameters and/or extensions.
|
- Custom commands have been removed. Use virtual parameters and/or extensions.
|
||||||
- Aliases and value normalizers (config/parameters.json) have been removed. Use
|
- Aliases and value normalizers (config/parameters.json) have been removed. Use
|
||||||
virtual parameters.
|
virtual parameters.
|
||||||
- The API /devices/<device_id>/preset has been removed.
|
- The API /devices/<device_id>/preset has been removed.
|
||||||
- Rarely used RequestDownload method no longer supported.
|
- Rarely used RequestDownload method no longer supported.
|
||||||
- The TR-069 client simulator has moved to its own repo at
|
- The TR-069 client simulator has moved to its own repo at
|
||||||
https://github.com/zaidka/genieacs-sim
|
https://github.com/zaidka/genieacs-sim
|
||||||
+9
-10
@@ -5,8 +5,8 @@ tracker is strictly for bug reports.
|
|||||||
|
|
||||||
# Issues
|
# Issues
|
||||||
|
|
||||||
We use GitHub issues to track bugs. Please ensure your bug description is clear
|
We use GitHub issues to track bugs. Please ensure your bug description is
|
||||||
and has sufficient instructions to be able to reproduce the issue.
|
clear and has sufficient instructions to be able to reproduce the issue.
|
||||||
|
|
||||||
If you face interoperability issues with your CPE, more often than not it's a
|
If you face interoperability issues with your CPE, more often than not it's a
|
||||||
device related issue. Please consult the [forum] before submitting an Issue.
|
device related issue. Please consult the [forum] before submitting an Issue.
|
||||||
@@ -26,14 +26,13 @@ By submitting a pull request to this repository, you acknowledge that, while
|
|||||||
maintaining copyright, you grant Zaid Abdulla a perpetual, worldwide, non-
|
maintaining copyright, you grant Zaid Abdulla a perpetual, worldwide, non-
|
||||||
exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare
|
exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare
|
||||||
derivative works of, publicly display, publicly perform, sublicense, and
|
derivative works of, publicly display, publicly perform, sublicense, and
|
||||||
distribute your contributions and such derivative works under the AGPLv3 license
|
distribute your contributions and such derivative works under the AGPLv3
|
||||||
or any other license terms, including, but not limited to, proprietary or
|
license or any other license terms, including, but not limited to, proprietary
|
||||||
commercial license terms.
|
or commercial license terms.
|
||||||
|
|
||||||
You confirm that you own or have rights to distribute and sublicense the source
|
You confirm that you own or have rights to distribute and sublicense the
|
||||||
code contained therein, and that your content does not infringe upon the
|
source code contained therein, and that your content does not infringe upon
|
||||||
intellectual property rights of a third party.
|
the intellectual property rights of a third party.
|
||||||
|
|
||||||
[commit-guidelines]:
|
[commit-guidelines]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||||
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
|
||||||
[forum]: https://forum.genieacs.com
|
[forum]: https://forum.genieacs.com
|
||||||
@@ -17,10 +17,10 @@ supported versions are:
|
|||||||
|
|
||||||
Install GenieACS from NPM:
|
Install GenieACS from NPM:
|
||||||
|
|
||||||
sudo npm install -g genieacs
|
sudo npm install -g --unsafe-perm genieacs
|
||||||
|
|
||||||
To build from source instead, clone this repo or download the source archive
|
To build from source instead, clone this repo or download the source archive
|
||||||
then _cd_ into the source directory then run:
|
then *cd* into the source directory then run:
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
@@ -30,8 +30,8 @@ source):
|
|||||||
|
|
||||||
### genieacs-cwmp
|
### genieacs-cwmp
|
||||||
|
|
||||||
This is the service that the CPEs will communicate with. It listens on port 7547
|
This is the service that the CPEs will communicate with. It listens on port
|
||||||
by default. Configure the ACS URL in your devices accordingly.
|
7547 by default. Configure the ACS URL in your devices accordingly.
|
||||||
|
|
||||||
You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim)
|
You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim)
|
||||||
as a dummy TR-069 simulator if you don't have a CPE at hand.
|
as a dummy TR-069 simulator if you don't have a CPE at hand.
|
||||||
@@ -50,7 +50,7 @@ such. It listens on port 7567 by default.
|
|||||||
### genieacs-ui
|
### genieacs-ui
|
||||||
|
|
||||||
This serves the web based user interface. It listens on port 3000 by default.
|
This serves the web based user interface. It listens on port 3000 by default.
|
||||||
You must pass _--ui-jwt-secret_ argument to supply the secret key used for
|
You must pass *--ui-jwt-secret* argument to supply the secret key used for
|
||||||
signing browser cookies:
|
signing browser cookies:
|
||||||
|
|
||||||
genieacs-ui --ui-jwt-secret secret
|
genieacs-ui --ui-jwt-secret secret
|
||||||
@@ -64,13 +64,14 @@ a complete installation guide for production deployments.
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
The [forum](https://forum.genieacs.com) is a good place to get guidance and help
|
The [forum](https://forum.genieacs.com) is a good place to get guidance and
|
||||||
from the community. Head on over and join the conversation!
|
help from the community. Head on over and join the conversation!
|
||||||
|
|
||||||
For commercial support options, please visit
|
For commercial support options, please visit
|
||||||
[genieacs.com](https://genieacs.com/support/).
|
[genieacs.com](https://genieacs.com/support/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2013-2020 GenieACS Inc. GenieACS is released under the
|
Copyright 2013-2020 GenieACS Inc. GenieACS is released under the [AGPLv3
|
||||||
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
license
|
||||||
|
terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
extensions: ["ts"],
|
"extensions": ["ts"],
|
||||||
require: ["esm", "ts-node/register/transpile-only"],
|
"require": ["esm", "ts-node/register/transpile-only"]
|
||||||
};
|
};
|
||||||
|
|
||||||
+3
-1
@@ -94,7 +94,9 @@ if (!cluster.worker) {
|
|||||||
|
|
||||||
const _listener = (req, res): void => {
|
const _listener = (req, res): void => {
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
void listener(req, res);
|
listener(req, res).catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
|
|||||||
+112
-39
@@ -25,11 +25,12 @@ import typescript from "@rollup/plugin-typescript";
|
|||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import webpack, { Configuration } from "webpack";
|
||||||
import postcss from "postcss";
|
import postcss from "postcss";
|
||||||
import postcssImport from "postcss-import";
|
import postcssImport from "postcss-import";
|
||||||
import postcssPresetEnv from "postcss-preset-env";
|
import postcssPresetEnv from "postcss-preset-env";
|
||||||
import cssnano from "cssnano";
|
import cssnano from "cssnano";
|
||||||
import { optimize, extendDefaultPlugins } from "svgo";
|
import SVGO from "svgo";
|
||||||
import * as xmlParser from "../lib/xml-parser";
|
import * as xmlParser from "../lib/xml-parser";
|
||||||
|
|
||||||
const MODE = process.env["NODE_ENV"] || "production";
|
const MODE = process.env["NODE_ENV"] || "production";
|
||||||
@@ -42,7 +43,7 @@ const BUILD_METADATA = new Date()
|
|||||||
const INPUT_DIR = path.resolve(__dirname, "..");
|
const INPUT_DIR = path.resolve(__dirname, "..");
|
||||||
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
||||||
|
|
||||||
const builtins = [
|
const externals = [
|
||||||
"path",
|
"path",
|
||||||
"fs",
|
"fs",
|
||||||
"cluster",
|
"cluster",
|
||||||
@@ -53,13 +54,33 @@ const builtins = [
|
|||||||
"zlib",
|
"zlib",
|
||||||
"crypto",
|
"crypto",
|
||||||
"util",
|
"util",
|
||||||
|
"mongodb",
|
||||||
"vm",
|
"vm",
|
||||||
|
"later",
|
||||||
|
"parsimmon",
|
||||||
|
"seedrandom",
|
||||||
"querystring",
|
"querystring",
|
||||||
"child_process",
|
"child_process",
|
||||||
"dgram",
|
"dgram",
|
||||||
"url",
|
"url",
|
||||||
"readline",
|
"iconv-lite",
|
||||||
|
"koa",
|
||||||
|
"koa-router",
|
||||||
|
"koa-compress",
|
||||||
|
"koa-bodyparser",
|
||||||
|
"koa-jwt",
|
||||||
|
"koa-static",
|
||||||
|
"jsonwebtoken",
|
||||||
"stream",
|
"stream",
|
||||||
|
"mithril",
|
||||||
|
"parsimmon",
|
||||||
|
"yaml",
|
||||||
|
"codemirror",
|
||||||
|
"codemirror/mode/javascript/javascript",
|
||||||
|
"codemirror/mode/yaml/yaml",
|
||||||
|
"ipaddr.js",
|
||||||
|
"jsbi",
|
||||||
|
"espresso-iisojs",
|
||||||
];
|
];
|
||||||
|
|
||||||
function rmDirSync(dirPath): void {
|
function rmDirSync(dirPath): void {
|
||||||
@@ -109,7 +130,7 @@ function generateSymbol(id: string, svgStr: string): string {
|
|||||||
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
|
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init(): Promise<string[]> {
|
async function init(): Promise<void> {
|
||||||
// Delete any old output directory
|
// Delete any old output directory
|
||||||
rmDirSync(OUTPUT_DIR);
|
rmDirSync(OUTPUT_DIR);
|
||||||
|
|
||||||
@@ -117,13 +138,13 @@ async function init(): Promise<string[]> {
|
|||||||
fs.mkdirSync(OUTPUT_DIR);
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
||||||
fs.mkdirSync(OUTPUT_DIR + "/public");
|
fs.mkdirSync(OUTPUT_DIR + "/public");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/tools");
|
||||||
|
|
||||||
// Create package.json
|
// Create package.json
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
||||||
);
|
);
|
||||||
delete packageJson["devDependencies"];
|
delete packageJson["devDependencies"];
|
||||||
delete packageJson["private"];
|
|
||||||
packageJson["scripts"] = {
|
packageJson["scripts"] = {
|
||||||
install: packageJson["scripts"].install,
|
install: packageJson["scripts"].install,
|
||||||
configure: packageJson["scripts"].configure,
|
configure: packageJson["scripts"].configure,
|
||||||
@@ -144,7 +165,6 @@ async function init(): Promise<string[]> {
|
|||||||
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
||||||
JSON.stringify(npmShrinkwrapJson, null, 2)
|
JSON.stringify(npmShrinkwrapJson, null, 2)
|
||||||
);
|
);
|
||||||
return Object.keys(packageJson["dependencies"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyStatic(): Promise<void> {
|
async function copyStatic(): Promise<void> {
|
||||||
@@ -182,7 +202,41 @@ async function generateCss(): Promise<void> {
|
|||||||
fs.writeFileSync(cssOutPath, cssOut.css);
|
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateBackendJs(externals: string[]): Promise<void> {
|
async function generateToolsJs(): Promise<void> {
|
||||||
|
for (const bin of ["dump-data-model"]) {
|
||||||
|
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}.ts`);
|
||||||
|
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: inputFile,
|
||||||
|
external: externals,
|
||||||
|
acorn: {
|
||||||
|
allowHashBang: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
include: [`tools/${bin}.ts`, "lib/**/*.ts"],
|
||||||
|
}),
|
||||||
|
MODE === "production" ? terser() : null,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await bundle.write({
|
||||||
|
format: "cjs",
|
||||||
|
preferConst: true,
|
||||||
|
sourcemap: "inline",
|
||||||
|
sourcemapExcludeSources: true,
|
||||||
|
banner: "#!/usr/bin/env node",
|
||||||
|
file: outputFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as executable
|
||||||
|
const mode = fs.statSync(outputFile).mode;
|
||||||
|
fs.chmodSync(outputFile, mode | 73);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateBackendJs(): Promise<void> {
|
||||||
for (const bin of [
|
for (const bin of [
|
||||||
"genieacs-cwmp",
|
"genieacs-cwmp",
|
||||||
"genieacs-ext",
|
"genieacs-ext",
|
||||||
@@ -194,7 +248,7 @@ async function generateBackendJs(externals: string[]): Promise<void> {
|
|||||||
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: inputFile,
|
input: inputFile,
|
||||||
external: [...builtins, ...externals],
|
external: externals,
|
||||||
acorn: {
|
acorn: {
|
||||||
allowHashBang: true,
|
allowHashBang: true,
|
||||||
},
|
},
|
||||||
@@ -238,26 +292,26 @@ async function generateBackendJs(externals: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateFrontendJs(externals: string[]): Promise<void> {
|
async function generateFrontendJs(): Promise<void> {
|
||||||
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
||||||
const outputDir = path.resolve(OUTPUT_DIR, "public");
|
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
|
||||||
|
|
||||||
const inlineDeps = ["parsimmon", "espresso-iisojs"];
|
const inlineDeps = ["mithril", "parsimmon", "jsbi", "espresso-iisojs"];
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: inputFile,
|
input: inputFile,
|
||||||
external: [
|
external: externals.filter((e) => !inlineDeps.includes(e)),
|
||||||
...builtins,
|
|
||||||
...externals.filter((e) => !inlineDeps.includes(e)),
|
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
rollupJson({ preferConst: true }),
|
rollupJson({ preferConst: true }),
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
resolveId: function (importee, importer) {
|
resolveId: function (importee, importer) {
|
||||||
if (importee.endsWith("/package.json")) {
|
if (importee.endsWith("/bigint")) {
|
||||||
const p = path.resolve(path.dirname(importer), importee);
|
return this.resolve(importee + "-jsbi", importer);
|
||||||
if (p === path.resolve(INPUT_DIR, "package.json"))
|
} else if (importee === "espresso-iisojs") {
|
||||||
return path.resolve(OUTPUT_DIR, "package.json");
|
return this.resolve(
|
||||||
|
"espresso-iisojs/dist/espresso-iisojs-jsbi.mjs",
|
||||||
|
importer
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -265,9 +319,8 @@ async function generateFrontendJs(externals: string[]): Promise<void> {
|
|||||||
typescript({ tsconfig: "./tsconfig.json" }),
|
typescript({ tsconfig: "./tsconfig.json" }),
|
||||||
nodeResolve(),
|
nodeResolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
MODE === "production" ? terser() : null,
|
|
||||||
],
|
],
|
||||||
preserveEntrySignatures: false,
|
inlineDynamicImports: true,
|
||||||
treeshake: {
|
treeshake: {
|
||||||
propertyReadSideEffects: false,
|
propertyReadSideEffects: false,
|
||||||
moduleSideEffects: false,
|
moduleSideEffects: false,
|
||||||
@@ -279,33 +332,52 @@ async function generateFrontendJs(externals: string[]): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await bundle.write({
|
await bundle.write({
|
||||||
manualChunks: (id) => {
|
|
||||||
if (id.includes("node_modules/codemirror")) return "codemirror";
|
|
||||||
else if (id.includes("node_modules/yaml")) return "yaml";
|
|
||||||
return "app";
|
|
||||||
},
|
|
||||||
preferConst: true,
|
preferConst: true,
|
||||||
format: "es",
|
format: "es",
|
||||||
sourcemap: true,
|
sourcemap: "inline",
|
||||||
sourcemapExcludeSources: true,
|
sourcemapExcludeSources: true,
|
||||||
dir: outputDir,
|
file: outputFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
const webpackConf: Configuration = {
|
||||||
|
mode: MODE as "development" | "production",
|
||||||
|
entry: outputFile,
|
||||||
|
resolve: {
|
||||||
|
aliasFields: ["module"],
|
||||||
|
},
|
||||||
|
devtool: "nosources-source-map",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(OUTPUT_DIR, "public"),
|
||||||
|
filename: "app.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
webpack(webpackConf, (err, stats) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
process.stdout.write(stats.toString({ colors: true }) + "\n");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateIconsSprite(): Promise<void> {
|
async function generateIconsSprite(): Promise<void> {
|
||||||
|
const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
|
||||||
const symbols = [];
|
const symbols = [];
|
||||||
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
||||||
for (const file of fs.readdirSync(iconsDir)) {
|
for (const file of fs.readdirSync(iconsDir)) {
|
||||||
const id = path.parse(file).name;
|
const id = path.parse(file).name;
|
||||||
const filePath = path.join(iconsDir, file);
|
const filePath = path.join(iconsDir, file);
|
||||||
const { data } = await optimize(fs.readFileSync(filePath).toString(), {
|
const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
|
||||||
plugins: extendDefaultPlugins([
|
|
||||||
{
|
|
||||||
name: "removeViewBox",
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
symbols.push(generateSymbol(id, data));
|
symbols.push(generateSymbol(id, data));
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -315,13 +387,14 @@ async function generateIconsSprite(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
.then((externals) => {
|
.then(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
copyStatic(),
|
copyStatic(),
|
||||||
generateCss(),
|
generateCss(),
|
||||||
generateIconsSprite(),
|
generateIconsSprite(),
|
||||||
generateBackendJs(externals),
|
generateToolsJs(),
|
||||||
generateFrontendJs(externals),
|
generateBackendJs(),
|
||||||
|
generateFrontendJs(),
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Ignore
|
// Ignore
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
personal_ws-1.1 en 0
|
|
||||||
genieacs
|
|
||||||
GenieACS
|
|
||||||
javascript
|
|
||||||
sudo
|
|
||||||
config
|
|
||||||
cwmp
|
|
||||||
CPE
|
|
||||||
systemctl
|
|
||||||
nbi
|
|
||||||
CWMP
|
|
||||||
NBI
|
|
||||||
ACS
|
|
||||||
SSL
|
|
||||||
xsd
|
|
||||||
args
|
|
||||||
DeviceID
|
|
||||||
GENIEACS
|
|
||||||
env
|
|
||||||
param
|
|
||||||
arg
|
|
||||||
AUTH
|
|
||||||
igd
|
|
||||||
TCP
|
|
||||||
toctree
|
|
||||||
auth
|
|
||||||
EnvironmentFile
|
|
||||||
JSON
|
|
||||||
json
|
|
||||||
parameterValues
|
|
||||||
setParameterValues
|
|
||||||
stderr
|
|
||||||
stdout
|
|
||||||
attr
|
|
||||||
cfg
|
|
||||||
CPEs
|
|
||||||
ExecStart
|
|
||||||
faq
|
|
||||||
FileType
|
|
||||||
GenieACS's
|
|
||||||
JWT
|
|
||||||
maxdepth
|
|
||||||
npm
|
|
||||||
objectName
|
|
||||||
refreshObject
|
|
||||||
SerialNumber
|
|
||||||
systemd
|
|
||||||
usr
|
|
||||||
WantedBy
|
|
||||||
yaml
|
|
||||||
chown
|
|
||||||
Config
|
|
||||||
getParameterValues
|
|
||||||
https
|
|
||||||
InternetGatewayDevice
|
|
||||||
mkdir
|
|
||||||
OUI
|
|
||||||
sql
|
|
||||||
TODO
|
|
||||||
UDP
|
|
||||||
addObject
|
|
||||||
AGPLv
|
|
||||||
const
|
|
||||||
cpe
|
|
||||||
cron
|
|
||||||
deleteObject
|
|
||||||
FactoryReset
|
|
||||||
factoryReset
|
|
||||||
FileName
|
|
||||||
fileType
|
|
||||||
Github
|
|
||||||
hostname
|
|
||||||
HTTPS
|
|
||||||
jwt
|
|
||||||
latlong
|
|
||||||
NPM
|
|
||||||
oui
|
|
||||||
parameterNames
|
|
||||||
ProductClass
|
|
||||||
productClass
|
|
||||||
repo
|
|
||||||
SSID
|
|
||||||
sublicense
|
|
||||||
TLS
|
|
||||||
WANIPConnection
|
|
||||||
WPA
|
|
||||||
YourProvisionName
|
|
||||||
abc
|
|
||||||
Abdulla
|
|
||||||
accessors
|
|
||||||
AddObject
|
|
||||||
AddressingType
|
|
||||||
api
|
|
||||||
APIs
|
|
||||||
chmod
|
|
||||||
cwmp's
|
|
||||||
dateext
|
|
||||||
DATETIME
|
|
||||||
dateTime
|
|
||||||
datetime
|
|
||||||
declaratively
|
|
||||||
delaycompress
|
|
||||||
DeleteObject
|
|
||||||
deviceId
|
|
||||||
DHCP
|
|
||||||
dir
|
|
||||||
distro
|
|
||||||
equisatisfiable
|
|
||||||
ExternalIPAddress
|
|
||||||
failover
|
|
||||||
favor
|
|
||||||
func
|
|
||||||
GetParameterAttributes
|
|
||||||
getPassword
|
|
||||||
gte
|
|
||||||
gui
|
|
||||||
hexBinary
|
|
||||||
Hils
|
|
||||||
HOSTNAME
|
|
||||||
init
|
|
||||||
journalctl
|
|
||||||
journald
|
|
||||||
LastFileName
|
|
||||||
LastFileType
|
|
||||||
lastInform
|
|
||||||
LIBXMLJS
|
|
||||||
libxmljs
|
|
||||||
literalinclude
|
|
||||||
lockfile
|
|
||||||
logrotate
|
|
||||||
lte
|
|
||||||
mipsbe
|
|
||||||
normalizers
|
|
||||||
pre
|
|
||||||
quickstart
|
|
||||||
README
|
|
||||||
redis
|
|
||||||
reimplemented
|
|
||||||
RequestDownload
|
|
||||||
RESTful
|
|
||||||
rollout
|
|
||||||
sandboxed
|
|
||||||
scalable
|
|
||||||
SetParameterAttributes
|
|
||||||
SetParamteerValues
|
|
||||||
SIGINT
|
|
||||||
SIGTERM
|
|
||||||
skipWritableCheck
|
|
||||||
stringify
|
|
||||||
subdirectory
|
|
||||||
TargetFileName
|
|
||||||
useradd
|
|
||||||
VirtualParameters
|
|
||||||
vparam
|
|
||||||
xml
|
|
||||||
YAML
|
|
||||||
Zaid
|
|
||||||
cacheExpire
|
|
||||||
iss
|
|
||||||
http
|
|
||||||
statusCode
|
|
||||||
rawData
|
|
||||||
pos
|
|
||||||
Brotli
|
|
||||||
CSV
|
|
||||||
hostInfo
|
|
||||||
IPv
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#! /bin/sh
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
FILES=`ls ../docs/*.rst ../docs/*.js ../*.md`
|
|
||||||
|
|
||||||
for FILE in $FILES
|
|
||||||
do
|
|
||||||
echo $FILE
|
|
||||||
cat $FILE | aspell list --lang=en --add-extra-dicts=./spellcheck-dict.pws --ignore 2
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
+16
-16
@@ -1,7 +1,7 @@
|
|||||||
API Reference
|
API Reference
|
||||||
=============
|
=============
|
||||||
|
|
||||||
GenieACS exposes a rich RESTful API through its NBI component. This document
|
GenieACS expoeses a rich RESTful API through its NBI component. This document
|
||||||
serves as a reference for the available APIs.
|
serves as a reference for the available APIs.
|
||||||
|
|
||||||
This API makes use of MongoDB's query language in some of its endpoints. Refer
|
This API makes use of MongoDB's query language in some of its endpoints. Refer
|
||||||
@@ -16,7 +16,7 @@ to MongoDB's documentation for details.
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
A common pitfall is not properly percent-encoding special characters in the
|
A common pitfll is not properly percent-encoding special characters in the
|
||||||
device ID or query in the URL.
|
device ID or query in the URL.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
@@ -41,11 +41,11 @@ Examples
|
|||||||
|
|
||||||
.. code:: javascript
|
.. code:: javascript
|
||||||
|
|
||||||
query = {"_id": "202BC1-BM632w-000000"}
|
query = {"_id": "202BC1-BM632w-0000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%22%7D'
|
curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D'
|
||||||
|
|
||||||
- Find a device by its MAC address:
|
- Find a device by its MAC address:
|
||||||
|
|
||||||
@@ -77,21 +77,21 @@ Examples
|
|||||||
|
|
||||||
.. code:: javascript
|
.. code:: javascript
|
||||||
|
|
||||||
query = {"device": "202BC1-BM632w-000000"}
|
query = {"device": "202BC1-BM632w-0000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-000000%22%7D'
|
curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-0000000%22%7D'
|
||||||
|
|
||||||
- Return specific parameters for a given device:
|
- Return specific parameters for a given device:
|
||||||
|
|
||||||
.. code:: javascript
|
.. code:: javascript
|
||||||
|
|
||||||
query = {"_id": "202BC1-BM632w-000000"}
|
query = {"_id": "202BC1-BM632w-0000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%22%7D&projection=InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.Manufacturer'
|
curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D&projection=InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.Manufacturer'
|
||||||
|
|
||||||
The ``projection`` URL param is a comma-separated list of the parameters to receive.
|
The ``projection`` URL param is a comma-separated list of the parameters to receive.
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ if the tasks have been queued to be executed at the next inform.
|
|||||||
*device_id*: The ID of the device.
|
*device_id*: The ID of the device.
|
||||||
|
|
||||||
*connection_request*: Indicates that a connection request will be triggered to
|
*connection_request*: Indicates that a connection request will be triggered to
|
||||||
execute the tasks immediately. Otherwise, the tasks will be queued and be
|
execute the tasks immediatly. Otherwise, the tasks will be queued and be
|
||||||
processed at the next inform.
|
processed at the next inform.
|
||||||
|
|
||||||
The response body is the task object as it is inserted in the database. The
|
The response body is the task object as it is inserted in the database. The
|
||||||
@@ -120,7 +120,7 @@ Examples
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tasks?connection_request' \
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tasks?connection_request' \
|
||||||
-X POST \
|
-X POST \
|
||||||
--data '{"name": "refreshObject", "objectName": ""}'
|
--data '{"name": "refreshObject", "objectName": ""}'
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ Examples
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tasks?connection_request' \
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tasks?connection_request' \
|
||||||
-X POST \
|
-X POST \
|
||||||
--data '{"name":"setParameterValues", "parameterValues": [["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "GenieACS", "xsd:string"],["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", "hello world", "xsd:string"]]}'
|
--data '{"name":"setParameterValues", "parameterValues": [["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "GenieACS", "xsd:string"],["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", "hello world", "xsd:string"]]}'
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ Example
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/faults/202BC1-BM632w-000000:default' -X DELETE
|
curl -i 'http://localhost:7557/faults/202BC1-BM632w-0000000:default' -X DELETE
|
||||||
|
|
||||||
DELETE /devices/\<device_id\>
|
DELETE /devices/\<device_id\>
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -218,7 +218,7 @@ Assign the tag "testing" to a device:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tags/testing' -X POST
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tags/testing' -X POST
|
||||||
|
|
||||||
DELETE /devices/\<device_id\>/tags/\<tag\>
|
DELETE /devices/\<device_id\>/tags/\<tag\>
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -236,7 +236,7 @@ Remove the tag "testing" from a device:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tags/testing' -X DELETE
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tags/testing' -X DELETE
|
||||||
|
|
||||||
PUT /presets/\<preset_name\>
|
PUT /presets/\<preset_name\>
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -299,7 +299,7 @@ The following file metadata may be sent as request headers:
|
|||||||
- ``fileType``: For firmware images it should be "1 Firmware Upgrade Image".
|
- ``fileType``: For firmware images it should be "1 Firmware Upgrade Image".
|
||||||
Other common types are "2 Web Content" and "3 Vendor Configuration File".
|
Other common types are "2 Web Content" and "3 Vendor Configuration File".
|
||||||
|
|
||||||
- ``oui``: The OUI of the device model that this file belongs to.
|
- ``oui``: The OUI of the device model that this file belogs to.
|
||||||
|
|
||||||
- ``productClass``: The product class of the device.
|
- ``productClass``: The product class of the device.
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ Find files using a query.
|
|||||||
Tasks
|
Tasks
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Find the different available tasks and their object structure.
|
Find the different availabe tasks and their object structure.
|
||||||
|
|
||||||
``getParameterValues``
|
``getParameterValues``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -3,70 +3,4 @@
|
|||||||
CPE Authentication
|
CPE Authentication
|
||||||
==================
|
==================
|
||||||
|
|
||||||
CPE to ACS
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
By default GenieACS will accept any incoming connection via HTTP/HTTPS and
|
|
||||||
respond to it.
|
|
||||||
|
|
||||||
The following parameters are used to set and get (password is redacted but
|
|
||||||
can be set) the username/password used to authenticate against the ACS:
|
|
||||||
|
|
||||||
Username: ``Device.ManagementServer.Username`` or ``InternetGatewayDevice.ManagementServer.Username``
|
|
||||||
|
|
||||||
Password: ``Device.ManagementServer.Password`` or ``InternetGatewayDevice.ManagementServer.Password``
|
|
||||||
|
|
||||||
Enable CPE to ACS Authentication
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
CPE to ACS authentication can be configured in the web interface by using the
|
|
||||||
`Config` option in the `Admin` tab.
|
|
||||||
|
|
||||||
Go to the `Admin` -> `Config` page and click on `New config` button at the
|
|
||||||
bottom of the page. This will open pop-up which requires you to fill in a key
|
|
||||||
and value. The key should be ``cwmp.auth``. The value accepts a boolean.
|
|
||||||
Setting the value to ``true`` makes it so that GenieACS accepts any incoming
|
|
||||||
connection, setting it to ``false`` makes GenieACS deny all incoming
|
|
||||||
connections. This can be further configured using the ``AUTH()`` and ``EXT()``
|
|
||||||
functions.
|
|
||||||
|
|
||||||
The ``AUTH()`` function
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``AUTH()`` function accepts two parameters, username and password. It
|
|
||||||
checks the given username and password with the incoming request to determine
|
|
||||||
whether to return true or false.
|
|
||||||
|
|
||||||
Basic usage of the ``AUTH()`` function could be as follows:
|
|
||||||
|
|
||||||
.. code:: sql
|
|
||||||
|
|
||||||
AUTH("fixed-username", "fixed-password")
|
|
||||||
|
|
||||||
This will only accept incoming request who authenticate with
|
|
||||||
"fixed-username" and "fixed-password".
|
|
||||||
|
|
||||||
The various device parameters can be referenced from within the ``cwmp.auth``
|
|
||||||
expression. For example:
|
|
||||||
|
|
||||||
.. code:: sql
|
|
||||||
|
|
||||||
AUTH(Device.ManagementServer.Username, Device.ManagementServer.Password)
|
|
||||||
|
|
||||||
The ``EXT()`` function
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``EXT()`` function makes it possible to call an :ref:`extension
|
|
||||||
<extensions>` script from the auth expression. This can be used to fetch
|
|
||||||
the credentials from an external source:
|
|
||||||
|
|
||||||
.. code:: sql
|
|
||||||
|
|
||||||
AUTH(DeviceID.SerialNumber, EXT("authenticate", "getPassword", DeviceID.SerialNumber))
|
|
||||||
|
|
||||||
ACS to CPE
|
|
||||||
----------
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
@@ -86,7 +86,7 @@ CWMP_ACCESS_LOG_FILE
|
|||||||
File to log incoming requests for genieacs-cwmp. If omitted, logs will go to
|
File to log incoming requests for genieacs-cwmp. If omitted, logs will go to
|
||||||
stdout.
|
stdout.
|
||||||
|
|
||||||
Default: unset
|
Default: unest
|
||||||
|
|
||||||
NBI_WORKER_PROCESSES
|
NBI_WORKER_PROCESSES
|
||||||
The number of worker processes to spawn for genieacs-nbi. A value of 0 means
|
The number of worker processes to spawn for genieacs-nbi. A value of 0 means
|
||||||
@@ -107,7 +107,7 @@ NBI_INTERFACE
|
|||||||
NBI_SSL_CERT
|
NBI_SSL_CERT
|
||||||
Path to certificate file. If omitted, non-secure HTTP will be used.
|
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
Default: unset
|
Default: unest
|
||||||
|
|
||||||
NBI_SSL_KEY
|
NBI_SSL_KEY
|
||||||
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||||
|
|||||||
+16
-19
@@ -1,4 +1,4 @@
|
|||||||
// This is an example GenieACS extension to get the current latitude/longitude
|
// This is an exmaple GenieACS extension to get the current latitude/longitude
|
||||||
// of the International Space Station. Why, you ask? Because why not.
|
// of the International Space Station. Why, you ask? Because why not.
|
||||||
// To install, copy this file to config/ext/iss.js.
|
// To install, copy this file to config/ext/iss.js.
|
||||||
|
|
||||||
@@ -10,28 +10,25 @@ let cache = null;
|
|||||||
let cacheExpire = 0;
|
let cacheExpire = 0;
|
||||||
|
|
||||||
function latlong(args, callback) {
|
function latlong(args, callback) {
|
||||||
if (Date.now() < cacheExpire) return callback(null, cache);
|
if (Date.now() < cacheExpire)
|
||||||
|
return callback(null, cache);
|
||||||
|
|
||||||
http
|
http.get("http://api.open-notify.org/iss-now.json", (res) => {
|
||||||
.get("http://api.open-notify.org/iss-now.json", (res) => {
|
if (res.statusCode !== 200)
|
||||||
if (res.statusCode !== 200)
|
return callback(new Error(`Request failed (status code: ${res.statusCode})`));
|
||||||
return callback(
|
|
||||||
new Error(`Request failed (status code: ${res.statusCode})`)
|
|
||||||
);
|
|
||||||
|
|
||||||
let rawData = "";
|
let rawData = "";
|
||||||
res.on("data", (chunk) => (rawData += chunk));
|
res.on("data", (chunk) => rawData += chunk);
|
||||||
|
|
||||||
res.on("end", () => {
|
res.on("end", () => {
|
||||||
let pos = JSON.parse(rawData)["iss_position"];
|
let pos = JSON.parse(rawData)["iss_position"];
|
||||||
cache = [+pos["latitude"], +pos["longitude"]];
|
cache = [+pos["latitude"], +pos["longitude"]];
|
||||||
cacheExpire = Date.now() + 10000;
|
cacheExpire = Date.now() + 10000;
|
||||||
callback(null, cache);
|
callback(null, cache);
|
||||||
});
|
|
||||||
})
|
|
||||||
.on("error", (err) => {
|
|
||||||
callback(err);
|
|
||||||
});
|
});
|
||||||
|
}).on("error", (err) => {
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latlong = latlong;
|
exports.latlong = latlong;
|
||||||
@@ -4,7 +4,7 @@ Installation Guide
|
|||||||
This guide is for installing GenieACS on a single server on any Linux distro
|
This guide is for installing GenieACS on a single server on any Linux distro
|
||||||
that uses *systemd* as its init system.
|
that uses *systemd* as its init system.
|
||||||
|
|
||||||
The various GenieACS services are independent of each other and may be
|
The various GenieACS services are independant of each other and may be
|
||||||
installed on different servers. You may also run multiple instances of each in
|
installed on different servers. You may also run multiple instances of each in
|
||||||
a load-balancing/failover setup.
|
a load-balancing/failover setup.
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Install GenieACS
|
|||||||
|
|
||||||
.. parsed-literal::
|
.. parsed-literal::
|
||||||
|
|
||||||
sudo npm install -g genieacs@\ |release|
|
sudo npm install -g --unsafe-perm genieacs@\ |release|
|
||||||
|
|
||||||
.. topic:: Installing from source
|
.. topic:: Installing from source
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ Configure systemd
|
|||||||
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
||||||
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
||||||
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
||||||
NODE_OPTIONS=--enable-source-maps
|
|
||||||
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
||||||
GENIEACS_UI_JWT_SECRET=secret
|
GENIEACS_UI_JWT_SECRET=secret
|
||||||
|
|
||||||
|
|||||||
+13
-27
@@ -145,24 +145,15 @@ export async function connectionRequest(
|
|||||||
|
|
||||||
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
||||||
|
|
||||||
let udpProm = Promise.resolve(false);
|
let udpProm;
|
||||||
if (udpConnectionRequestAddress) {
|
if (udpConnectionRequestAddress) {
|
||||||
try {
|
udpProm = udpConnectionRequest(
|
||||||
const u = new URL("udp://" + udpConnectionRequestAddress);
|
udpConnectionRequestAddress,
|
||||||
udpProm = udpConnectionRequest(
|
authExp,
|
||||||
u.hostname,
|
UDP_CONNECTION_REQUEST_PORT,
|
||||||
parseInt(u.port || "80"),
|
debug,
|
||||||
authExp,
|
deviceId
|
||||||
UDP_CONNECTION_REQUEST_PORT,
|
);
|
||||||
debug,
|
|
||||||
deviceId
|
|
||||||
).then(
|
|
||||||
() => true,
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore invalid address
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await httpConnectionRequest(
|
const status = await httpConnectionRequest(
|
||||||
@@ -174,7 +165,10 @@ export async function connectionRequest(
|
|||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (await udpProm) return "";
|
if (udpProm) {
|
||||||
|
await udpProm;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -302,15 +296,6 @@ function sanitizeTask(task): void {
|
|||||||
)
|
)
|
||||||
throw new Error("Invalid 'provisions' property");
|
throw new Error("Invalid 'provisions' property");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "reboot":
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "factoryReset":
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error("Invalid task name");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
@@ -348,5 +333,6 @@ export async function deleteDevice(deviceId: string): Promise<void> {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
cache.del(`${deviceId}_tasks_faults_operations`),
|
cache.del(`${deviceId}_tasks_faults_operations`),
|
||||||
|
db.deleteDeviceUploads(deviceId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -51,7 +51,7 @@ export function escapeRegExp(str: string): string {
|
|||||||
export function encodeTag(tag: string): string {
|
export function encodeTag(tag: string): string {
|
||||||
return encodeURIComponent(tag)
|
return encodeURIComponent(tag)
|
||||||
.replace(
|
.replace(
|
||||||
/[!~*'().]/g,
|
/[!~*'()]/g,
|
||||||
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
||||||
)
|
)
|
||||||
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2020 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import JSBI from "jsbi";
|
||||||
|
|
||||||
|
export function and(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.bitwiseAnd(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function or(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.bitwiseOr(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xor(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.bitwiseXor(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function not(a: JSBI): JSBI {
|
||||||
|
return JSBI.bitwiseNot(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lshift(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.leftShift(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rshift(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.signedRightShift(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function add(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.add(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sub(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.subtract(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mul(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.multiply(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function div(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.divide(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exp(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.exponentiate(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rem(a: JSBI, b: JSBI): JSBI {
|
||||||
|
return JSBI.remainder(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toNumber(a: JSBI): number {
|
||||||
|
return JSBI.toNumber(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function eq(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.equal(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ne(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.notEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lt(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.lessThan(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lte(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.lessThanOrEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gt(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.greaterThan(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gte(a: JSBI, b: JSBI): boolean {
|
||||||
|
return JSBI.greaterThanOrEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asUintN(a: number, b: JSBI): JSBI {
|
||||||
|
return JSBI.asUintN(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asIntN(a: number, b: JSBI): JSBI {
|
||||||
|
return JSBI.asIntN(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _BigInt = JSBI.BigInt;
|
||||||
|
type _bigint = JSBI;
|
||||||
|
export { _BigInt as BigInt, _bigint as bigint };
|
||||||
@@ -130,15 +130,6 @@ export function evaluateCallback(exp: Expression): Expression {
|
|||||||
} else if (exp[1] === "LOWER") {
|
} else if (exp[1] === "LOWER") {
|
||||||
if (exp[2] == null) return null;
|
if (exp[2] == null) return null;
|
||||||
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
||||||
} else if (exp[1] === "ROUND") {
|
|
||||||
const n = exp[2];
|
|
||||||
const p = exp.length > 3 ? exp[3] : 0;
|
|
||||||
if (n == null || p == null) return null;
|
|
||||||
if (!isArray(n) && !isArray(p)) {
|
|
||||||
const d = 10 ** p;
|
|
||||||
const m = n * d * (1 + Number.EPSILON);
|
|
||||||
return Math.round(m) / d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (exp[0] === "PARAM") {
|
} else if (exp[0] === "PARAM") {
|
||||||
if (exp[1] == null) return null;
|
if (exp[1] == null) return null;
|
||||||
|
|||||||
+3
-12
@@ -47,18 +47,9 @@ export default function memoize<T extends (...args: any[]) => any>(func: T): T {
|
|||||||
if (cache1.has(key)) return cache1.get(key);
|
if (cache1.has(key)) return cache1.get(key);
|
||||||
|
|
||||||
let r;
|
let r;
|
||||||
if (cache2.has(key)) {
|
if (cache2.has(key)) r = cache2.get(key);
|
||||||
cache1.set(key, (r = cache2.get(key)));
|
else r = func(...args);
|
||||||
} else {
|
cache1.set(key, r);
|
||||||
cache1.set(key, (r = func(...args)));
|
|
||||||
// Evict rejected promises
|
|
||||||
if (r instanceof Promise) {
|
|
||||||
r.catch(() => {
|
|
||||||
cache1.delete(key);
|
|
||||||
cache2.delete(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r;
|
return r;
|
||||||
}) as any;
|
}) as any;
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-104
@@ -23,31 +23,6 @@ type Alias = [Path, string][];
|
|||||||
let cache1 = new Map<string, Path>();
|
let cache1 = new Map<string, Path>();
|
||||||
let cache2 = new Map<string, Path>();
|
let cache2 = new Map<string, Path>();
|
||||||
|
|
||||||
const CHAR_SPACE = 32;
|
|
||||||
const CHAR_BACKSLASH = 92;
|
|
||||||
const CHAR_COLON = 58;
|
|
||||||
const CHAR_QUOTE = 34;
|
|
||||||
const CHAR_OPEN_BRACKET = 91;
|
|
||||||
const CHAR_CLOSE_BRACKET = 93;
|
|
||||||
const CHAR_ASTERISK = 42;
|
|
||||||
const CHAR_COMMA = 44;
|
|
||||||
const CHAR_DOT = 46;
|
|
||||||
|
|
||||||
function charCodeAt(str: string, index: number): number {
|
|
||||||
if (index >= str.length) return 0;
|
|
||||||
return str.charCodeAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function legalChar(c: number): boolean {
|
|
||||||
return (
|
|
||||||
(c >= 97 && c <= 122) ||
|
|
||||||
(c >= 65 && c <= 90) ||
|
|
||||||
(c >= 48 && c <= 57) ||
|
|
||||||
c === 95 ||
|
|
||||||
c === 45
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Path {
|
export default class Path {
|
||||||
public readonly segments: Segments;
|
public readonly segments: Segments;
|
||||||
public readonly wildcard: number;
|
public readonly wildcard: number;
|
||||||
@@ -55,70 +30,37 @@ export default class Path {
|
|||||||
protected _string: string;
|
protected _string: string;
|
||||||
protected _stringIndex: number[];
|
protected _stringIndex: number[];
|
||||||
|
|
||||||
protected static parseAliasValue(
|
|
||||||
pattern: string,
|
|
||||||
index: number
|
|
||||||
): { index: number; value: string } {
|
|
||||||
let i = index;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
|
|
||||||
for (let j = i + 1; j < pattern.length; ++j) {
|
|
||||||
if (
|
|
||||||
pattern.charCodeAt(j) === CHAR_QUOTE &&
|
|
||||||
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
++j;
|
|
||||||
const v = JSON.parse(pattern.slice(i, j));
|
|
||||||
return { index: j, value: v };
|
|
||||||
} catch {
|
|
||||||
return { index, value: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { index, value: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < pattern.length; ++i) {
|
|
||||||
const c = pattern.charCodeAt(i);
|
|
||||||
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { index: i, value: pattern.slice(index, i).trim() };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static parseAliasPath(
|
|
||||||
pattern: string,
|
|
||||||
index: number
|
|
||||||
): { index: number; path: Path } {
|
|
||||||
let i = index;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
const { index: idx, segments } = Path.parsePath(pattern, i);
|
|
||||||
if (idx === i) return { index, path: null };
|
|
||||||
i = idx;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
|
|
||||||
return { index: i + 1, path: new Path(segments) };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static parseAlias(
|
protected static parseAlias(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
index: number
|
index: number
|
||||||
): { index: number; alias: Alias } {
|
): { index: number; alias: Alias } {
|
||||||
const alias: Alias = [];
|
const alias: Alias = [];
|
||||||
let i = index;
|
while (index < pattern.length && pattern[index] !== "]") {
|
||||||
for (;;) {
|
const { index: idx, segments } = Path.parsePath(pattern, index);
|
||||||
let path: Path;
|
let j = (index = idx + 1);
|
||||||
let value: string;
|
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
||||||
({ index: i, path } = Path.parseAliasPath(pattern, i));
|
if (pattern[j] === '"' && index === j) {
|
||||||
if (!path) break;
|
++j;
|
||||||
({ index: i, value } = Path.parseAliasValue(pattern, i));
|
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
|
||||||
if (value == null) break;
|
if (++j >= pattern.length)
|
||||||
alias.push([path, value]);
|
throw new Error("Invalid alias expression");
|
||||||
index = i;
|
}
|
||||||
|
}
|
||||||
|
if (++j >= pattern.length) throw new Error("Invalid alias expression");
|
||||||
|
}
|
||||||
|
|
||||||
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
|
let value = pattern.slice(index, j).trim();
|
||||||
++i;
|
index = j;
|
||||||
|
if (value[0] === '"') {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Invalid alias expression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alias.push([new Path(segments), value]);
|
||||||
|
if (pattern[index] === ",") ++index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure identical expressions have idential string representation
|
// Ensure identical expressions have idential string representation
|
||||||
@@ -139,26 +81,30 @@ export default class Path {
|
|||||||
index: number
|
index: number
|
||||||
): { index: number; segments: Segments } {
|
): { index: number; segments: Segments } {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
|
// Colon separator is needed for parseAlias
|
||||||
|
if (index < pattern.length && pattern[index] !== ":") {
|
||||||
|
for (;;) {
|
||||||
|
if (pattern[index] === "[") {
|
||||||
|
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
|
||||||
|
index = idx + 1;
|
||||||
|
segments.push(alias);
|
||||||
|
} else {
|
||||||
|
const j = index;
|
||||||
|
while (
|
||||||
|
index < pattern.length &&
|
||||||
|
pattern[index] !== ":" &&
|
||||||
|
pattern[index] !== "."
|
||||||
|
)
|
||||||
|
++index;
|
||||||
|
const s = pattern.slice(j, index).trim();
|
||||||
|
if (!s) throw new Error("Invalid parameter path");
|
||||||
|
segments.push(s);
|
||||||
|
}
|
||||||
|
|
||||||
let segStart = index;
|
if (index >= pattern.length || pattern[index] === ":") break;
|
||||||
let wildcard = -1;
|
else if (pattern[index] !== ".")
|
||||||
for (let i = index; i <= pattern.length; ++i) {
|
throw new Error("Invalid alias expression");
|
||||||
const c = charCodeAt(pattern, i);
|
++index;
|
||||||
if (c === CHAR_OPEN_BRACKET && i === segStart) {
|
|
||||||
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
|
|
||||||
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
|
|
||||||
segments.push(alias);
|
|
||||||
index = i = idx + 1;
|
|
||||||
segStart = i + 1;
|
|
||||||
} else if (c === CHAR_ASTERISK && i === segStart) {
|
|
||||||
wildcard = i + 1;
|
|
||||||
} else if (!legalChar(c) || i === wildcard) {
|
|
||||||
if (i === segStart) break;
|
|
||||||
const s = pattern.slice(segStart, i);
|
|
||||||
segments.push(s);
|
|
||||||
index = i;
|
|
||||||
if (c !== CHAR_DOT) break;
|
|
||||||
segStart = i + 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +143,7 @@ export default class Path {
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const { index, segments } = Path.parsePath(str, 0);
|
const { segments } = Path.parsePath(str, 0);
|
||||||
if (index < str.length) throw new Error("Invalid parameter path");
|
|
||||||
path = new Path(segments);
|
path = new Path(segments);
|
||||||
if (path.toString() !== str) cache1.set(path.toString(), path);
|
if (path.toString() !== str) cache1.set(path.toString(), path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
const LINE_WIDTH = 80;
|
|
||||||
const INDENTATION = " ";
|
|
||||||
|
|
||||||
const STRING_RESERVED = new Set([
|
|
||||||
"true",
|
|
||||||
"True",
|
|
||||||
"TRUE",
|
|
||||||
"false",
|
|
||||||
"False",
|
|
||||||
"FALSE",
|
|
||||||
"null",
|
|
||||||
"Null",
|
|
||||||
"NULL",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function isPrintable(str: string): boolean {
|
|
||||||
return !/[^\t\n\x20-\x7e\x85\u{a0}-\u{d7ff}\u{e000}-\u{fffd}\u{10000}-\u{10ffff}]/u.test(
|
|
||||||
str
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyKey(str: string): string {
|
|
||||||
if (!str || !isPrintable(str)) return JSON.stringify(str);
|
|
||||||
if (/^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|[\n,[\]{}]|\s$/.test(str))
|
|
||||||
return JSON.stringify(str);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function foldString(str: string): string[] {
|
|
||||||
if (str.length <= LINE_WIDTH) return [str];
|
|
||||||
if (str.startsWith(" ")) return [str];
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
let cand = 0;
|
|
||||||
for (let i = 1; i < str.length - 1; ++i) {
|
|
||||||
if (str[i] !== " ") continue;
|
|
||||||
|
|
||||||
if (str[i + 1] === " ") {
|
|
||||||
i += 2;
|
|
||||||
while (str[i] === " ") ++i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i <= idx + LINE_WIDTH) {
|
|
||||||
cand = i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = cand > idx ? cand : i;
|
|
||||||
lines.push(str.slice(idx, c));
|
|
||||||
idx = c + 1;
|
|
||||||
cand = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cand > idx && str.length > idx + LINE_WIDTH) {
|
|
||||||
lines.push(str.slice(idx, cand));
|
|
||||||
idx = cand + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(str.slice(idx));
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyString(str: string, res: string[], prefix1, prefix2): void {
|
|
||||||
if (/^\s*$/.test(str) || STRING_RESERVED.has(str) || !isPrintable(str)) {
|
|
||||||
res.push(prefix1 + JSON.stringify(str));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefix2) prefix2 = INDENTATION;
|
|
||||||
|
|
||||||
const lines = str.split("\n");
|
|
||||||
if (lines.length > 1) {
|
|
||||||
let idt = "";
|
|
||||||
let chmp = "-";
|
|
||||||
if ((lines.find((l) => l) || "").startsWith(" "))
|
|
||||||
idt = `${INDENTATION.length}`;
|
|
||||||
|
|
||||||
if (!lines[lines.length - 1]) {
|
|
||||||
lines.pop();
|
|
||||||
if (lines[lines.length - 1]) chmp = "";
|
|
||||||
else chmp = "+";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^\s+$/.test(lines[lines.length - 1])) {
|
|
||||||
res.push(prefix1 + JSON.stringify(str));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFolded = false;
|
|
||||||
const folded = lines.map((l) => {
|
|
||||||
const ls = foldString(l);
|
|
||||||
if (ls.length > 1) isFolded = true;
|
|
||||||
return ls;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isFolded) {
|
|
||||||
res.push(
|
|
||||||
`${prefix1}|${idt}${chmp}`,
|
|
||||||
...lines.map((l) => (l ? prefix2 + l : l))
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.push(`${prefix1}>${idt}${chmp}`);
|
|
||||||
res.push(...folded[0].map((f) => prefix2 + f));
|
|
||||||
for (let i = 1; i < folded.length; ++i) {
|
|
||||||
const prevLine = folded[i - 1][0];
|
|
||||||
if (prevLine && !folded[i - 1][0].startsWith(" ")) res.push("");
|
|
||||||
res.push(...folded[i].map((f) => prefix2 + f));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
/^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|\s$/.test(str) ||
|
|
||||||
parseFloat(str) === +str
|
|
||||||
) {
|
|
||||||
res.push(prefix1 + JSON.stringify(str));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.push(prefix1 + str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyAny(
|
|
||||||
obj: unknown,
|
|
||||||
res: string[],
|
|
||||||
prefix1 = "",
|
|
||||||
prefix2 = ""
|
|
||||||
): void {
|
|
||||||
if (obj == null) {
|
|
||||||
res.push(`${prefix1}null`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof obj === "number" || typeof obj === "boolean") {
|
|
||||||
res.push(`${prefix1}${JSON.stringify(obj)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (obj instanceof Date) {
|
|
||||||
res.push(`${prefix1}${obj.toJSON()}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof obj === "string") {
|
|
||||||
stringifyString(obj, res, prefix1, prefix2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
if (!obj.length) {
|
|
||||||
res.push(prefix1 + "[]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefix1 || prefix1.endsWith("- ")) {
|
|
||||||
stringifyAny(obj[0], res, prefix1 + "- ", prefix2 + INDENTATION);
|
|
||||||
prefix1 = prefix2 + "- ";
|
|
||||||
prefix2 = prefix2 + INDENTATION;
|
|
||||||
for (let i = 1; i < obj.length; ++i)
|
|
||||||
stringifyAny(obj[i], res, prefix1, prefix2);
|
|
||||||
} else {
|
|
||||||
res.push(prefix1);
|
|
||||||
prefix1 = prefix2 + "- ";
|
|
||||||
prefix2 = prefix2 + INDENTATION;
|
|
||||||
for (let i = 0; i < obj.length; ++i)
|
|
||||||
stringifyAny(obj[i], res, prefix1, prefix2);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = Object.entries(obj).filter((e) => e[1] !== undefined);
|
|
||||||
|
|
||||||
if (!entries.length) {
|
|
||||||
res.push(prefix1 + "{}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefix1 || prefix1.endsWith("- ")) {
|
|
||||||
stringifyAny(
|
|
||||||
entries[0][1],
|
|
||||||
res,
|
|
||||||
prefix1 + `${stringifyKey(entries[0][0])}: `,
|
|
||||||
prefix2 + INDENTATION
|
|
||||||
);
|
|
||||||
prefix1 = prefix2;
|
|
||||||
prefix2 = prefix2 + INDENTATION;
|
|
||||||
for (let i = 1; i < entries.length; ++i) {
|
|
||||||
stringifyAny(
|
|
||||||
entries[i][1],
|
|
||||||
res,
|
|
||||||
prefix1 + `${stringifyKey(entries[i][0])}: `,
|
|
||||||
prefix2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.push(prefix1);
|
|
||||||
prefix1 = prefix2;
|
|
||||||
prefix2 = prefix2 + INDENTATION;
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
|
||||||
stringifyAny(
|
|
||||||
entries[i][1],
|
|
||||||
res,
|
|
||||||
prefix1 + `${stringifyKey(entries[i][0])}: `,
|
|
||||||
prefix2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringify(obj: unknown): string {
|
|
||||||
if (obj === undefined) return undefined;
|
|
||||||
const lines: string[] = [];
|
|
||||||
stringifyAny(obj, lines);
|
|
||||||
return lines.join("\n") + "\n";
|
|
||||||
}
|
|
||||||
@@ -169,13 +169,9 @@ export async function httpConnectionRequest(
|
|||||||
if (res.statusCode === 503) return "Device is offline";
|
if (res.statusCode === 503) return "Device is offline";
|
||||||
|
|
||||||
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
||||||
try {
|
authHeader = auth.parseWwwAuthenticateHeader(
|
||||||
authHeader = auth.parseWwwAuthenticateHeader(
|
res.headers["www-authenticate"]
|
||||||
res.headers["www-authenticate"]
|
);
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return "Connection request error: Error parsing www-authenticate header";
|
|
||||||
}
|
|
||||||
[username, password, authExp] = await extractAuth(authExp, false);
|
[username, password, authExp] = await extractAuth(authExp, false);
|
||||||
} else {
|
} else {
|
||||||
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
||||||
@@ -186,13 +182,14 @@ export async function httpConnectionRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function udpConnectionRequest(
|
export async function udpConnectionRequest(
|
||||||
host: string,
|
address: string,
|
||||||
port: number,
|
|
||||||
authExp: Expression,
|
authExp: Expression,
|
||||||
sourcePort = 0,
|
sourcePort = 0,
|
||||||
_debug: boolean,
|
_debug: boolean,
|
||||||
deviceId: string
|
deviceId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const [host, portStr] = address.split(":", 2);
|
||||||
|
const port = portStr ? parseInt(portStr) : 80;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
||||||
@@ -219,12 +216,12 @@ export async function udpConnectionRequest(
|
|||||||
.createHmac("sha1", password)
|
.createHmac("sha1", password)
|
||||||
.update(`${ts}${id}${username}${cn}`)
|
.update(`${ts}${id}${username}${cn}`)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
const uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
||||||
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`;
|
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`;
|
||||||
const message = Buffer.from(msg);
|
const message = Buffer.from(msg);
|
||||||
|
|
||||||
for (let i = 0; i < 3; ++i) {
|
for (let i = 0; i < 3; ++i) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
client.send(message, 0, message.length, port, host, (err: Error) => {
|
client.send(message, 0, message.length, port, host, (err: Error) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
|
|||||||
+31
-84
@@ -79,18 +79,14 @@ async function authenticate(
|
|||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.auth"
|
"cwmp.auth"
|
||||||
);
|
);
|
||||||
if (authExpression == null) return true;
|
if (!authExpression) return true;
|
||||||
|
|
||||||
let authentication;
|
let authentication;
|
||||||
|
|
||||||
if (sessionContext.httpRequest.headers["authorization"]) {
|
if (sessionContext.httpRequest.headers["authorization"]) {
|
||||||
try {
|
authentication = auth.parseAuthorizationHeader(
|
||||||
authentication = auth.parseAuthorizationHeader(
|
sessionContext.httpRequest.headers["authorization"]
|
||||||
sessionContext.httpRequest.headers["authorization"]
|
);
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authentication && authentication.method === "Digest") {
|
if (authentication && authentication.method === "Digest") {
|
||||||
@@ -212,7 +208,7 @@ async function writeResponse(
|
|||||||
sessionContext.extendLock = now + 10000;
|
sessionContext.extendLock = now + 10000;
|
||||||
await cache.acquireLock(
|
await cache.acquireLock(
|
||||||
`cwmp_session_${sessionContext.deviceId}`,
|
`cwmp_session_${sessionContext.deviceId}`,
|
||||||
sessionContext.timeout * 1000 + 15000,
|
sessionContext.timeout + 10000,
|
||||||
0,
|
0,
|
||||||
sessionContext.sessionId
|
sessionContext.sessionId
|
||||||
);
|
);
|
||||||
@@ -361,39 +357,6 @@ async function transferComplete(sessionContext, rpc): Promise<void> {
|
|||||||
return writeResponse(sessionContext, res);
|
return writeResponse(sessionContext, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duStateChangeComplete(sessionContext, rpc): Promise<void> {
|
|
||||||
const { acsResponse, operation, fault } = await session.duStateChangeComplete(
|
|
||||||
sessionContext,
|
|
||||||
rpc.cpeRequest
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!operation) {
|
|
||||||
logger.accessWarn({
|
|
||||||
sessionContext: sessionContext,
|
|
||||||
message: "Unrecognized command key",
|
|
||||||
rpc: rpc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fault) {
|
|
||||||
Object.assign(sessionContext.retries, operation.retries);
|
|
||||||
recordFault(
|
|
||||||
sessionContext,
|
|
||||||
fault,
|
|
||||||
operation.provisions,
|
|
||||||
operation.channels
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = soap.response({
|
|
||||||
id: rpc.id,
|
|
||||||
acsResponse: acsResponse,
|
|
||||||
cwmpVersion: sessionContext.cwmpVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
return writeResponse(sessionContext, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append provisions and remove duplicates
|
// Append provisions and remove duplicates
|
||||||
function appendProvisions(original, toAppend): boolean {
|
function appendProvisions(original, toAppend): boolean {
|
||||||
let modified = false;
|
let modified = false;
|
||||||
@@ -742,6 +705,11 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
|||||||
["download", task.fileType, task.fileName, task.targetFileName || ""],
|
["download", task.fileType, task.fileName, task.targetFileName || ""],
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
case "upload":
|
||||||
|
session.addProvisions(sessionContext, `task_${task._id}`, [
|
||||||
|
["upload", task.fileType, task.fileName],
|
||||||
|
]);
|
||||||
|
break;
|
||||||
case "addObject":
|
case "addObject":
|
||||||
alias = (task.parameterValues || [])
|
alias = (task.parameterValues || [])
|
||||||
.map((p) => `${p[0]}:${JSON.stringify(p[1])}`)
|
.map((p) => `${p[0]}:${JSON.stringify(p[1])}`)
|
||||||
@@ -763,15 +731,7 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (!sessionContext.doneTasks) sessionContext.doneTasks = [];
|
throw new Error("Task name not recognized");
|
||||||
sessionContext.doneTasks.push(task._id);
|
|
||||||
sessionContext.tasks = sessionContext.tasks.filter((t) => t !== task);
|
|
||||||
|
|
||||||
logger.accessWarn({
|
|
||||||
sessionContext: sessionContext,
|
|
||||||
message: "Invalid task",
|
|
||||||
taskId: task._id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextRpc(sessionContext);
|
return nextRpc(sessionContext);
|
||||||
@@ -895,6 +855,21 @@ async function sendAcsRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (acsRequest.name === "Upload") {
|
||||||
|
let prefix = "" + config.get("FS_URL_PREFIX");
|
||||||
|
|
||||||
|
if (!prefix) {
|
||||||
|
const FS_PORT = +config.get("FS_PORT");
|
||||||
|
const ssl = !!config.get("FS_SSL_CERT");
|
||||||
|
const origin = getRequestOrigin(sessionContext.httpRequest);
|
||||||
|
let hostname = origin.localAddress;
|
||||||
|
if (origin.host) [hostname] = origin.host.split(":", 1);
|
||||||
|
prefix = (ssl ? "https" : "http") + `://${hostname}:${FS_PORT}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
acsRequest.url = prefix + encodeURI(acsRequest.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
const rpc = {
|
const rpc = {
|
||||||
id: id,
|
id: id,
|
||||||
acsRequest: acsRequest,
|
acsRequest: acsRequest,
|
||||||
@@ -928,7 +903,7 @@ async function getSession(
|
|||||||
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
||||||
if (!sessionContextString) return null;
|
if (!sessionContextString) return null;
|
||||||
sessionContext = await session.deserialize(sessionContextString);
|
sessionContext = await session.deserialize(sessionContextString);
|
||||||
connection.setTimeout(sessionContext.timeout * 1000);
|
connection.setTimeout(sessionContext.timeout);
|
||||||
return sessionContext;
|
return sessionContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,14 +921,6 @@ export function onConnection(socket: Socket): void {
|
|||||||
const sessionContext = currentSessions.get(socket);
|
const sessionContext = currentSessions.get(socket);
|
||||||
if (!sessionContext) return;
|
if (!sessionContext) return;
|
||||||
currentSessions.delete(socket);
|
currentSessions.delete(socket);
|
||||||
if (sessionContext.authState !== 2) {
|
|
||||||
logger.accessError({
|
|
||||||
message: "Authentication failure",
|
|
||||||
sessionContext: sessionContext,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const lastActivity = sessionContext.lastActivity;
|
const lastActivity = sessionContext.lastActivity;
|
||||||
@@ -1183,7 +1150,7 @@ async function processRequest(
|
|||||||
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
||||||
await cache.acquireLock(
|
await cache.acquireLock(
|
||||||
`cwmp_session_${sessionContext.deviceId}`,
|
`cwmp_session_${sessionContext.deviceId}`,
|
||||||
sessionContext.timeout * 1000 + 15000,
|
sessionContext.timeout + 10000,
|
||||||
0,
|
0,
|
||||||
sessionContext.sessionId
|
sessionContext.sessionId
|
||||||
);
|
);
|
||||||
@@ -1272,16 +1239,6 @@ async function processRequest(
|
|||||||
cwmpVersion: sessionContext.cwmpVersion,
|
cwmpVersion: sessionContext.cwmpVersion,
|
||||||
});
|
});
|
||||||
return writeResponse(sessionContext, res);
|
return writeResponse(sessionContext, res);
|
||||||
} else if (rpc.cpeRequest.name === "DUStateChangeComplete") {
|
|
||||||
if (sessionContext.state !== 1) return reportBadState(sessionContext);
|
|
||||||
|
|
||||||
logger.accessInfo({
|
|
||||||
sessionContext: sessionContext,
|
|
||||||
message: "CPE request",
|
|
||||||
rpc: rpc,
|
|
||||||
});
|
|
||||||
|
|
||||||
return duStateChangeComplete(sessionContext, rpc);
|
|
||||||
} else {
|
} else {
|
||||||
if (sessionContext.state !== 1 || rpc.cpeRequest.name === "Inform")
|
if (sessionContext.state !== 1 || rpc.cpeRequest.name === "Inform")
|
||||||
return void reportBadState(sessionContext);
|
return void reportBadState(sessionContext);
|
||||||
@@ -1291,15 +1248,7 @@ async function processRequest(
|
|||||||
} else if (rpc.cpeResponse) {
|
} else if (rpc.cpeResponse) {
|
||||||
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
||||||
|
|
||||||
const fault = await session.rpcResponse(
|
await session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse);
|
||||||
sessionContext,
|
|
||||||
rpc.id,
|
|
||||||
rpc.cpeResponse
|
|
||||||
);
|
|
||||||
if (fault) {
|
|
||||||
recordFault(sessionContext, fault);
|
|
||||||
session.clearProvisions(sessionContext);
|
|
||||||
}
|
|
||||||
return nextRpc(sessionContext);
|
return nextRpc(sessionContext);
|
||||||
} else if (rpc.cpeFault) {
|
} else if (rpc.cpeFault) {
|
||||||
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
||||||
@@ -1641,10 +1590,8 @@ async function listenerAsync(
|
|||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (d) {
|
if (d)
|
||||||
debug.incomingHttpRequest(httpRequest, null, bodyStr);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
|
||||||
debug.outgoingHttpResponse(httpResponse, null, _body);
|
|
||||||
}
|
|
||||||
httpResponse.end(_body);
|
httpResponse.end(_body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MongoClient, ObjectID, Collection, Db } from "mongodb";
|
import { MongoClient, ObjectID, Collection, Db, GridFSBucket } from "mongodb";
|
||||||
import { get } from "./config";
|
import { get } from "./config";
|
||||||
import { decodeTag, encodeTag, escapeRegExp } from "./common";
|
import { encodeTag, escapeRegExp } from "./common";
|
||||||
import { parse } from "./common/expression-parser";
|
import { parse } from "./common/expression-parser";
|
||||||
import {
|
import {
|
||||||
DeviceData,
|
DeviceData,
|
||||||
@@ -39,15 +39,15 @@ export let tasksCollection: Collection,
|
|||||||
virtualParametersCollection: Collection,
|
virtualParametersCollection: Collection,
|
||||||
faultsCollection: Collection,
|
faultsCollection: Collection,
|
||||||
filesCollection: Collection,
|
filesCollection: Collection,
|
||||||
|
uploadsCollection: Collection,
|
||||||
operationsCollection: Collection,
|
operationsCollection: Collection,
|
||||||
permissionsCollection: Collection,
|
permissionsCollection: Collection,
|
||||||
usersCollection: Collection,
|
usersCollection: Collection,
|
||||||
configCollection: Collection;
|
configCollection: Collection,
|
||||||
|
uploadsBucket: GridFSBucket;
|
||||||
|
|
||||||
let clientPromise: Promise<MongoClient>;
|
let clientPromise: Promise<MongoClient>;
|
||||||
|
|
||||||
const INVALID_PATH_SUFFIX = "__invalid";
|
|
||||||
|
|
||||||
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||||
if (list1.length !== list2.length) return false;
|
if (list1.length !== list2.length) return false;
|
||||||
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||||
@@ -79,6 +79,7 @@ onConnect(async (db) => {
|
|||||||
presetsCollection = db.collection("presets");
|
presetsCollection = db.collection("presets");
|
||||||
objectsCollection = db.collection("objects");
|
objectsCollection = db.collection("objects");
|
||||||
filesCollection = db.collection("fs.files");
|
filesCollection = db.collection("fs.files");
|
||||||
|
uploadsCollection = db.collection("uploads.files");
|
||||||
provisionsCollection = db.collection("provisions");
|
provisionsCollection = db.collection("provisions");
|
||||||
virtualParametersCollection = db.collection("virtualParameters");
|
virtualParametersCollection = db.collection("virtualParameters");
|
||||||
faultsCollection = db.collection("faults");
|
faultsCollection = db.collection("faults");
|
||||||
@@ -86,6 +87,7 @@ onConnect(async (db) => {
|
|||||||
permissionsCollection = db.collection("permissions");
|
permissionsCollection = db.collection("permissions");
|
||||||
usersCollection = db.collection("users");
|
usersCollection = db.collection("users");
|
||||||
configCollection = db.collection("config");
|
configCollection = db.collection("config");
|
||||||
|
uploadsBucket = new GridFSBucket(db, { bucketName: "uploads" });
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function disconnect(): Promise<void> {
|
export async function disconnect(): Promise<void> {
|
||||||
@@ -133,12 +135,7 @@ export async function fetchDevice(
|
|||||||
const device = await devicesCollection.findOne({ _id: id });
|
const device = await devicesCollection.findOne({ _id: id });
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
|
|
||||||
function storeParams(
|
function storeParams(obj, path: string, pathLength: number, ts): void {
|
||||||
obj,
|
|
||||||
path: string,
|
|
||||||
pathLength: number,
|
|
||||||
ts: number
|
|
||||||
): void {
|
|
||||||
if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"];
|
if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"];
|
||||||
if (obj["_attributesTimestamp"])
|
if (obj["_attributesTimestamp"])
|
||||||
obj["_attributesTimestamp"] = +obj["_attributesTimestamp"];
|
obj["_attributesTimestamp"] = +obj["_attributesTimestamp"];
|
||||||
@@ -169,34 +166,19 @@ export async function fetchDevice(
|
|||||||
if (obj["_accessList"] != null)
|
if (obj["_accessList"] != null)
|
||||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||||
|
|
||||||
try {
|
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
|
||||||
res.push([Path.parse(path), t, attrs]);
|
|
||||||
} catch (err) {
|
|
||||||
// The path parser is now more strict so we might be in a situation where
|
|
||||||
// the database contains invalid paths from before this change So here we
|
|
||||||
// encode the invalid characters.
|
|
||||||
const splits = path.split(".");
|
|
||||||
splits[splits.length - 1] =
|
|
||||||
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
|
|
||||||
path = splits.join(".");
|
|
||||||
res.push([Path.parse(path), t, attrs]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
if (!k.startsWith("_")) {
|
if (!k.startsWith("_")) {
|
||||||
obj["_object"] = true;
|
obj["_object"] = true;
|
||||||
storeParams(v, `${path}.${k}`, pathLength + 1, obj["_timestamp"]);
|
storeParams(v, path + k + ".", pathLength + 1, obj["_timestamp"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj["_object"] && obj["_timestamp"])
|
if ((obj["_object"] || !pathLength) && obj["_timestamp"])
|
||||||
res.push([Path.parse(path + ".*"), obj["_timestamp"]]);
|
res.push([Path.parse(path + "*"), obj["_timestamp"]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ts: number = device["_timestamp"] || 0;
|
|
||||||
if (ts) res.push([Path.parse("*"), ts]);
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(device)) {
|
for (const [k, v] of Object.entries(device)) {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case "_lastInform":
|
case "_lastInform":
|
||||||
@@ -324,11 +306,10 @@ export async function fetchDevice(
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (!k.startsWith("_")) storeParams(v, k, 1, ts);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storeParams(device, "", 0, 0);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,17 +443,19 @@ export async function saveDevice(
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (
|
||||||
|
diff[0].segments[0] === "Uploads" &&
|
||||||
|
diff[0].segments[2] === "LastFileName" &&
|
||||||
|
value1 &&
|
||||||
|
value1 !== value2
|
||||||
|
) {
|
||||||
|
uploadsBucket.delete(value1 as any, () => {
|
||||||
|
// Ignore error due to mising files
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!diff[2]) {
|
if (!diff[2]) {
|
||||||
let pathStr = path.toString();
|
update["$unset"][path.toString()] = 1;
|
||||||
// Paths with that suffix are encoded and need to be decoded
|
|
||||||
if (pathStr.endsWith(INVALID_PATH_SUFFIX)) {
|
|
||||||
const splits = pathStr.split(".");
|
|
||||||
splits[splits.length - 1] = decodeTag(
|
|
||||||
splits[splits.length - 1].slice(0, 0 - INVALID_PATH_SUFFIX.length)
|
|
||||||
);
|
|
||||||
pathStr = splits.join(".");
|
|
||||||
}
|
|
||||||
update["$unset"][pathStr] = 1;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,3 +803,14 @@ interface User {
|
|||||||
export async function getUsers(): Promise<User[]> {
|
export async function getUsers(): Promise<User[]> {
|
||||||
return usersCollection.find().toArray();
|
return usersCollection.find().toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteDeviceUploads(deviceId: string): Promise<void> {
|
||||||
|
const files = await uploadsCollection
|
||||||
|
.find({
|
||||||
|
_id: {
|
||||||
|
$regex: `^${escapeRegExp(deviceId)}\\/`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
await Promise.all(files.map((f) => uploadsBucket.delete(f["_id"])));
|
||||||
|
}
|
||||||
+7
-7
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "http";
|
} from "http";
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
import { appendFileSync } from "fs";
|
import { appendFileSync } from "fs";
|
||||||
import { stringify } from "./common/yaml";
|
import * as yaml from "yaml";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
|
|
||||||
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
||||||
@@ -64,7 +64,7 @@ export function incomingHttpRequest(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
@@ -90,7 +90,7 @@ export function outgoingHttpResponse(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
@@ -119,7 +119,7 @@ export function outgoingHttpRequest(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
@@ -147,7 +147,7 @@ export function outgoingHttpRequestError(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
@@ -173,7 +173,7 @@ export function incomingHttpResponse(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
@@ -197,7 +197,7 @@ export function outgoingUdpMessage(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
||||||
else if (DEBUG_FORMAT === "json")
|
else if (DEBUG_FORMAT === "json")
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
||||||
|
|||||||
@@ -229,6 +229,44 @@ export function download(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function upload(
|
||||||
|
sessionContext: SessionContext,
|
||||||
|
provision: (string | number | boolean)[],
|
||||||
|
declarations: Declaration[]
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
provision.length !== 3 ||
|
||||||
|
typeof provision[1] !== "string" ||
|
||||||
|
typeof provision[2] !== "string"
|
||||||
|
)
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
|
const alias = [
|
||||||
|
`FileType:${JSON.stringify(provision[1] || "")}`,
|
||||||
|
`FileName:${JSON.stringify(provision[2] || "")}`,
|
||||||
|
].join(",");
|
||||||
|
|
||||||
|
declarations.push({
|
||||||
|
path: Path.parse(`Uploads.[${alias}]`),
|
||||||
|
pathGet: 1,
|
||||||
|
pathSet: 1,
|
||||||
|
attrGet: null,
|
||||||
|
attrSet: null,
|
||||||
|
defer: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
declarations.push({
|
||||||
|
path: Path.parse(`Uploads.[${alias}].Upload`),
|
||||||
|
pathGet: 1,
|
||||||
|
pathSet: null,
|
||||||
|
attrGet: { value: 1 },
|
||||||
|
attrSet: { value: [sessionContext.timestamp] },
|
||||||
|
defer: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function instances(
|
export function instances(
|
||||||
sessionContext: SessionContext,
|
sessionContext: SessionContext,
|
||||||
provision: (string | number | boolean)[],
|
provision: (string | number | boolean)[],
|
||||||
|
|||||||
+10
-23
@@ -22,14 +22,20 @@ import * as crypto from "crypto";
|
|||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
import { Fault } from "./types";
|
import { Fault } from "./types";
|
||||||
import { ROOT_DIR } from "./config";
|
import { ROOT_DIR } from "./config";
|
||||||
import * as logger from "./logger";
|
|
||||||
import readline from "readline";
|
|
||||||
|
|
||||||
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
||||||
|
|
||||||
const processes: { [script: string]: ChildProcess } = {};
|
const processes: { [script: string]: ChildProcess } = {};
|
||||||
const jobs = new Map();
|
const jobs = new Map();
|
||||||
|
|
||||||
|
function messageHandler(message): void {
|
||||||
|
const func = jobs.get(message[0]);
|
||||||
|
if (func) {
|
||||||
|
jobs.delete(message[0]);
|
||||||
|
func({ fault: message[1], value: message[2] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const scriptName = args[0];
|
const scriptName = args[0];
|
||||||
@@ -39,7 +45,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
|||||||
|
|
||||||
if (!processes[scriptName]) {
|
if (!processes[scriptName]) {
|
||||||
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
||||||
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||||
});
|
});
|
||||||
processes[scriptName] = p;
|
processes[scriptName] = p;
|
||||||
|
|
||||||
@@ -62,26 +68,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
|||||||
if (processes[scriptName] === p) delete processes[scriptName];
|
if (processes[scriptName] === p) delete processes[scriptName];
|
||||||
});
|
});
|
||||||
|
|
||||||
p.on("message", (message) => {
|
p.on("message", messageHandler);
|
||||||
const func = jobs.get(message[0]);
|
|
||||||
if (func) {
|
|
||||||
jobs.delete(message[0]);
|
|
||||||
// Wait for any disconnect even to fire
|
|
||||||
setTimeout(() => {
|
|
||||||
func({ fault: message[1], value: message[2] });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlstdout = readline.createInterface(p.stdout);
|
|
||||||
rlstdout.on("line", (line) => {
|
|
||||||
logger.info({ message: `Ext ${scriptName}(${p.pid}): ${line}` });
|
|
||||||
});
|
|
||||||
|
|
||||||
const rlstderr = readline.createInterface(p.stderr);
|
|
||||||
rlstderr.on("line", (line) => {
|
|
||||||
logger.warn({ message: `Ext ${scriptName}(${p.pid}): ${line}` });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -20,75 +20,106 @@
|
|||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import * as querystring from "querystring";
|
import * as querystring from "querystring";
|
||||||
import { IncomingMessage, ServerResponse } from "http";
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
import { Collection, GridFSBucket } from "mongodb";
|
import { Collection, GridFSBucket, ObjectId } from "mongodb";
|
||||||
import { onConnect } from "./db";
|
import { onConnect, getOperations } from "./db";
|
||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
import { getRequestOrigin } from "./forwarded";
|
import { getRequestOrigin } from "./forwarded";
|
||||||
import memoize from "./common/memoize";
|
|
||||||
|
|
||||||
let filesCollection: Collection;
|
let filesCollection: Collection;
|
||||||
let filesBucket: GridFSBucket;
|
let filesBucket: GridFSBucket;
|
||||||
|
let uploadsBucket: GridFSBucket;
|
||||||
|
|
||||||
onConnect(async (db) => {
|
onConnect(async (db) => {
|
||||||
filesCollection = db.collection("fs.files");
|
filesCollection = db.collection("fs.files");
|
||||||
filesBucket = new GridFSBucket(db);
|
filesBucket = new GridFSBucket(db);
|
||||||
|
uploadsBucket = new GridFSBucket(db, { bucketName: "uploads" });
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFile = memoize(
|
async function canUpload(
|
||||||
(md5: string, size: number, filename: string): Promise<Buffer> => {
|
deviceId: string,
|
||||||
return new Promise((resolve, reject) => {
|
fileName: string,
|
||||||
const buffer = Buffer.allocUnsafe(size);
|
timeout = Date.now() + 5000
|
||||||
let i = 0;
|
): Promise<boolean> {
|
||||||
const downloadStream = filesBucket.openDownloadStreamByName(filename);
|
const operations = Object.values(await getOperations(deviceId));
|
||||||
downloadStream.on("error", reject);
|
for (const operation of operations) {
|
||||||
downloadStream.on("data", (data: Buffer) => {
|
if (operation.name === "Upload" && operation.args.fileName === fileName)
|
||||||
data.copy(buffer, i);
|
return true;
|
||||||
i += data.length;
|
|
||||||
});
|
|
||||||
downloadStream.on("end", () => {
|
|
||||||
if (i !== size) reject(new Error("File size mismatch"));
|
|
||||||
else resolve(buffer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
if (Date.now() >= timeout) return false;
|
||||||
|
// Need to wait and retry in case upload was initiated before session was closed
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
return canUpload(deviceId, fileName, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listener(
|
export async function listener(
|
||||||
request: IncomingMessage,
|
request: IncomingMessage,
|
||||||
response: ServerResponse
|
response: ServerResponse
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (request.method !== "GET") {
|
const urlParts = url.parse(request.url, true);
|
||||||
|
if (request.method === "GET") {
|
||||||
|
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
message: "Fetch file",
|
||||||
|
filename: filename,
|
||||||
|
remoteAddress: getRequestOrigin(request).remoteAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = await filesCollection.findOne({ _id: filename });
|
||||||
|
if (!file) {
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
|
log.message += " not found";
|
||||||
|
logger.accessError(log);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.writeHead(200, {
|
||||||
|
"Content-Type": file.contentType || "application/octet-stream",
|
||||||
|
"Content-Length": file.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadStream = filesBucket.openDownloadStreamByName(filename);
|
||||||
|
downloadStream.pipe(response);
|
||||||
|
|
||||||
|
logger.accessInfo(log);
|
||||||
|
} else if (request.method === "PUT") {
|
||||||
|
const [, deviceId, ...filePath] = urlParts.pathname
|
||||||
|
.split("/")
|
||||||
|
.map(querystring.unescape);
|
||||||
|
|
||||||
|
const fileName = `${deviceId}/${filePath.join("/")}`;
|
||||||
|
|
||||||
|
if (!(await canUpload(deviceId, fileName))) {
|
||||||
|
response.writeHead(403);
|
||||||
|
response.end("403 Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadsBucket.delete((fileName as unknown) as ObjectId);
|
||||||
|
} catch (err) {
|
||||||
|
// File not found, do nothing
|
||||||
|
}
|
||||||
|
const uploadStream = uploadsBucket.openUploadStreamWithId(
|
||||||
|
fileName,
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
uploadStream.on("finish", () => {
|
||||||
|
response.writeHead(200);
|
||||||
|
response.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadStream.on("error", (err) => {
|
||||||
|
response.writeHead(500);
|
||||||
|
response.end(err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.pipe(uploadStream);
|
||||||
|
} else {
|
||||||
response.writeHead(405, { Allow: "GET" });
|
response.writeHead(405, { Allow: "GET" });
|
||||||
response.end("405 Method Not Allowed");
|
response.end("405 Method Not Allowed");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlParts = url.parse(request.url, true);
|
|
||||||
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
|
||||||
|
|
||||||
const log = {
|
|
||||||
message: "Fetch file",
|
|
||||||
filename: filename,
|
|
||||||
remoteAddress: getRequestOrigin(request).remoteAddress,
|
|
||||||
};
|
|
||||||
|
|
||||||
const file = await filesCollection.findOne({ _id: filename });
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
response.writeHead(404);
|
|
||||||
response.end();
|
|
||||||
log.message += " not found";
|
|
||||||
logger.accessError(log);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await getFile(file.md5, file.length, filename);
|
|
||||||
|
|
||||||
response.writeHead(200, {
|
|
||||||
"Content-Type": file.contentType || "application/octet-stream",
|
|
||||||
"Content-Length": file.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
response.end(buffer);
|
|
||||||
logger.accessInfo(log);
|
|
||||||
}
|
}
|
||||||
@@ -108,6 +108,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
|
|||||||
{ role: "admin", resource: "devices", access: 3, validate: "true" },
|
{ role: "admin", resource: "devices", access: 3, validate: "true" },
|
||||||
{ role: "admin", resource: "faults", access: 3, validate: "true" },
|
{ role: "admin", resource: "faults", access: 3, validate: "true" },
|
||||||
{ role: "admin", resource: "files", access: 3, validate: "true" },
|
{ role: "admin", resource: "files", access: 3, validate: "true" },
|
||||||
|
{ role: "admin", resource: "uploads", access: 3, validate: "true" },
|
||||||
{ role: "admin", resource: "presets", access: 3, validate: "true" },
|
{ role: "admin", resource: "presets", access: 3, validate: "true" },
|
||||||
{ role: "admin", resource: "provisions", access: 3, validate: "true" },
|
{ role: "admin", resource: "provisions", access: 3, validate: "true" },
|
||||||
{ role: "admin", resource: "config", access: 3, validate: "true" },
|
{ role: "admin", resource: "config", access: 3, validate: "true" },
|
||||||
|
|||||||
+3
-3
@@ -50,8 +50,8 @@ interface Snapshot {
|
|||||||
ui: UiConfig;
|
ui: UiConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH = 5000;
|
const REFRESH = 3000;
|
||||||
const EVICT_TIMEOUT = 120000;
|
const EVICT_TIMEOUT = 60000;
|
||||||
|
|
||||||
const snapshots = new Map<string, Snapshot>();
|
const snapshots = new Map<string, Snapshot>();
|
||||||
let currentSnapshot: string = null;
|
let currentSnapshot: string = null;
|
||||||
@@ -398,7 +398,7 @@ async function refresh(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockToken = await cache.acquireLock("presets_hash_lock", 5000, 5000);
|
const lockToken = await cache.acquireLock("presets_hash_lock", 4000, 4000);
|
||||||
|
|
||||||
const res = await Promise.all([
|
const res = await Promise.all([
|
||||||
fetchPresets(),
|
fetchPresets(),
|
||||||
|
|||||||
@@ -640,6 +640,14 @@ export function flattenFile(
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenUpload(
|
||||||
|
file: Record<string, unknown>
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const f = {};
|
||||||
|
f["_id"] = file["_id"];
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
export function preProcessPreset(
|
export function preProcessPreset(
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
|
|||||||
+2
-14
@@ -29,7 +29,6 @@ import { IncomingMessage, ServerResponse } from "http";
|
|||||||
import * as cache from "./cache";
|
import * as cache from "./cache";
|
||||||
import { version as VERSION } from "../package.json";
|
import { version as VERSION } from "../package.json";
|
||||||
import { ping } from "./ping";
|
import { ping } from "./ping";
|
||||||
import * as logger from "./logger";
|
|
||||||
|
|
||||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
||||||
@@ -49,7 +48,7 @@ const collections = {
|
|||||||
devices: null as Collection,
|
devices: null as Collection,
|
||||||
presets: null as Collection,
|
presets: null as Collection,
|
||||||
objects: null as Collection,
|
objects: null as Collection,
|
||||||
files: null as Collection,
|
"fs.files": null as Collection,
|
||||||
provisions: null as Collection,
|
provisions: null as Collection,
|
||||||
virtualParameters: null as Collection,
|
virtualParameters: null as Collection,
|
||||||
faults: null as Collection,
|
faults: null as Collection,
|
||||||
@@ -57,10 +56,7 @@ const collections = {
|
|||||||
let filesBucket: GridFSBucket;
|
let filesBucket: GridFSBucket;
|
||||||
|
|
||||||
onConnect(async (db) => {
|
onConnect(async (db) => {
|
||||||
for (const k of Object.keys(collections)) {
|
for (const k of Object.keys(collections)) collections[k] = db.collection(k);
|
||||||
if (k === "files") collections[k] = db.collection("fs.files");
|
|
||||||
else collections[k] = db.collection(k);
|
|
||||||
}
|
|
||||||
filesBucket = new GridFSBucket(db);
|
filesBucket = new GridFSBucket(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,14 +95,6 @@ export function listener(
|
|||||||
request.addListener("end", () => {
|
request.addListener("end", () => {
|
||||||
const body = getBody();
|
const body = getBody();
|
||||||
const urlParts = url.parse(request.url, true);
|
const urlParts = url.parse(request.url, true);
|
||||||
|
|
||||||
logger.accessInfo(
|
|
||||||
Object.assign({}, urlParts.query, {
|
|
||||||
remoteAddress: request.connection.remoteAddress,
|
|
||||||
message: `${request.method} ${urlParts.pathname}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (PRESETS_REGEX.test(urlParts.pathname)) {
|
if (PRESETS_REGEX.test(urlParts.pathname)) {
|
||||||
const presetName = querystring.unescape(
|
const presetName = querystring.unescape(
|
||||||
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export function ping(
|
|||||||
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
||||||
): void {
|
): void {
|
||||||
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
||||||
host = host.replace("[", "").replace("]", "");
|
|
||||||
switch (platform()) {
|
switch (platform()) {
|
||||||
case "linux":
|
case "linux":
|
||||||
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
||||||
|
|||||||
+464
-544
File diff suppressed because it is too large.
Load diff
+58
-162
@@ -45,12 +45,10 @@ import {
|
|||||||
RebootResponse,
|
RebootResponse,
|
||||||
FactoryResetResponse,
|
FactoryResetResponse,
|
||||||
DownloadResponse,
|
DownloadResponse,
|
||||||
|
UploadResponse,
|
||||||
GetRPCMethodsRequest,
|
GetRPCMethodsRequest,
|
||||||
RequestDownloadRequest,
|
RequestDownloadRequest,
|
||||||
AcsResponse,
|
AcsResponse,
|
||||||
ChangeDUState,
|
|
||||||
ChangeDUStateResponse,
|
|
||||||
DUStateChangeCompleteRequest,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import Path from "./common/path";
|
import Path from "./common/path";
|
||||||
|
|
||||||
@@ -112,8 +110,8 @@ function event(xml: Element): string[] {
|
|||||||
|
|
||||||
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
|
.filter((e) => e.localName === "ParameterInfoStruct")
|
||||||
.map<[Path, boolean, boolean]>((e) => {
|
.map<[Path, boolean, boolean]>((e) => {
|
||||||
if (e.localName !== "ParameterInfoStruct") return null;
|
|
||||||
let param: string, value: string;
|
let param: string, value: string;
|
||||||
for (const c of e.children) {
|
for (const c of e.children) {
|
||||||
switch (c.localName) {
|
switch (c.localName) {
|
||||||
@@ -137,20 +135,10 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
|||||||
parsed = false;
|
parsed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (param && !param.endsWith("."))
|
||||||
if (param && !param.endsWith("."))
|
return [Path.parse(param), false, parsed];
|
||||||
return [Path.parse(param), false, parsed];
|
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
||||||
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
});
|
||||||
} catch (err) {
|
|
||||||
warnings.push({
|
|
||||||
message: "Missing or invalid XML node",
|
|
||||||
element: "Name",
|
|
||||||
parameter: param,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((e) => e != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getValueType = memoize((str: string) => {
|
const getValueType = memoize((str: string) => {
|
||||||
@@ -164,8 +152,8 @@ function parameterValueList(
|
|||||||
xml: Element
|
xml: Element
|
||||||
): [Path, string | number | boolean, string][] {
|
): [Path, string | number | boolean, string][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
|
.filter((e) => e.localName === "ParameterValueStruct")
|
||||||
.map<[Path, string | number | boolean, string]>((e) => {
|
.map<[Path, string | number | boolean, string]>((e) => {
|
||||||
if (e.localName !== "ParameterValueStruct") return null;
|
|
||||||
let valueElement: Element, param: string;
|
let valueElement: Element, param: string;
|
||||||
for (const c of e.children) {
|
for (const c of e.children) {
|
||||||
switch (c.localName) {
|
switch (c.localName) {
|
||||||
@@ -221,24 +209,15 @@ function parameterValueList(
|
|||||||
parsed = value;
|
parsed = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
return [Path.parse(param), parsed, valueType];
|
return [Path.parse(param), parsed, valueType];
|
||||||
} catch (err) {
|
});
|
||||||
warnings.push({
|
|
||||||
message: "Missing or invalid XML node",
|
|
||||||
element: "Name",
|
|
||||||
parameter: param,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((e) => e != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
|
.filter((e) => e.localName === "ParameterAttributeStruct")
|
||||||
.map<[Path, number, string[]]>((e) => {
|
.map<[Path, number, string[]]>((e) => {
|
||||||
if (e.localName !== "ParameterAttributeStruct") return null;
|
|
||||||
let notificationElement: Element,
|
let notificationElement: Element,
|
||||||
accessListElement: Element,
|
accessListElement: Element,
|
||||||
param: string;
|
param: string;
|
||||||
@@ -270,18 +249,8 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
|||||||
.filter((c) => c.localName === "string")
|
.filter((c) => c.localName === "string")
|
||||||
.map((c) => decodeEntities(c.text));
|
.map((c) => decodeEntities(c.text));
|
||||||
|
|
||||||
try {
|
return [Path.parse(param), notification, accessList];
|
||||||
return [Path.parse(param), notification, accessList];
|
});
|
||||||
} catch (err) {
|
|
||||||
warnings.push({
|
|
||||||
message: "Missing or invalid XML node",
|
|
||||||
element: "Name",
|
|
||||||
parameter: param,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((e) => e != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetParameterNames(methodRequest): string {
|
function GetParameterNames(methodRequest): string {
|
||||||
@@ -528,6 +497,24 @@ function Download(methodRequest): string {
|
|||||||
)}</FailureURL></cwmp:Download>`;
|
)}</FailureURL></cwmp:Download>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Upload(methodRequest): string {
|
||||||
|
return `<cwmp:Upload><CommandKey>${
|
||||||
|
methodRequest.commandKey || ""
|
||||||
|
}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
|
||||||
|
methodRequest.url
|
||||||
|
}</URL><Username>${encodeEntities(
|
||||||
|
methodRequest.username || ""
|
||||||
|
)}</Username><Password>${encodeEntities(
|
||||||
|
methodRequest.password || ""
|
||||||
|
)}</Password><DelaySeconds>${
|
||||||
|
methodRequest.delaySeconds || "0"
|
||||||
|
}</DelaySeconds><SuccessURL>${encodeEntities(
|
||||||
|
methodRequest.successUrl || ""
|
||||||
|
)}</SuccessURL><FailureURL>${encodeEntities(
|
||||||
|
methodRequest.failureUrl || ""
|
||||||
|
)}</FailureURL></cwmp:Upload>`;
|
||||||
|
}
|
||||||
|
|
||||||
function DownloadResponse(xml: Element): DownloadResponse {
|
function DownloadResponse(xml: Element): DownloadResponse {
|
||||||
let status: number, startTime: number, completeTime: number;
|
let status: number, startTime: number, completeTime: number;
|
||||||
for (const c of xml.children) {
|
for (const c of xml.children) {
|
||||||
@@ -576,26 +563,32 @@ function DownloadResponse(xml: Element): DownloadResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChangeDUState(methodRequest: ChangeDUState): string {
|
function UploadResponse(xml: Element): UploadResponse {
|
||||||
const operations = methodRequest.operations.map((op) => {
|
let status: number, startTime: number, completeTime: number;
|
||||||
if (op.operationType === "install")
|
for (const c of xml.children) {
|
||||||
return `<Operations xsi:type="cwmp:InstallOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
|
switch (c.localName) {
|
||||||
else if (op.operationType === "update")
|
case "Status":
|
||||||
return `<Operations xsi:type="cwmp:UpdateOpStruct"><UUID>${op.uuid}</UUID><URL>${op.url}</URL><Version>${op.version}</Version><Username>${op.username}</Username><Password>${op.password}</Password></Operations>`;
|
status = parseInt(c.text);
|
||||||
else if (op.operationType === "uninstall")
|
break;
|
||||||
return `<Operations xsi:type="cwmp:UninstallOpStruct"><UUID>${op.uuid}</UUID><Version>${op.version}</Version><ExecutionEnvRef>${op.executionEnvRef}</ExecutionEnvRef></Operations>`;
|
case "StartTime":
|
||||||
|
startTime = Date.parse(c.text);
|
||||||
|
break;
|
||||||
|
case "CompleteTime":
|
||||||
|
completeTime = Date.parse(c.text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
if (!(status >= 0)) {
|
||||||
});
|
warnings.push({ message: "Invalid UploadResponse status" });
|
||||||
|
status = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return `<cwmp:ChangeDUState><CommandKey>${
|
|
||||||
methodRequest.commandKey || ""
|
|
||||||
}</CommandKey>${operations.join("")}</cwmp:ChangeDUState>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChangeDUStateResponse(): ChangeDUStateResponse {
|
|
||||||
return {
|
return {
|
||||||
name: "ChangeDUStateResponse",
|
name: "UploadResponse",
|
||||||
|
status: status,
|
||||||
|
startTime: startTime,
|
||||||
|
completeTime: completeTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,97 +738,6 @@ function TransferCompleteResponse(): string {
|
|||||||
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
|
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function DUStateChangeComplete(xml: Element): DUStateChangeCompleteRequest {
|
|
||||||
let commandKey: string,
|
|
||||||
results: DUStateChangeCompleteRequest["results"],
|
|
||||||
res: DUStateChangeCompleteRequest["results"][0];
|
|
||||||
for (const c of xml.children) {
|
|
||||||
switch (c.localName) {
|
|
||||||
case "CommandKey":
|
|
||||||
commandKey = c.text;
|
|
||||||
break;
|
|
||||||
case "Results":
|
|
||||||
if (!results) results = [];
|
|
||||||
res = {
|
|
||||||
uuid: null,
|
|
||||||
deploymentUnitRef: null,
|
|
||||||
version: null,
|
|
||||||
currentState: null,
|
|
||||||
resolved: true,
|
|
||||||
executionUnitRefList: null,
|
|
||||||
startTime: null,
|
|
||||||
completeTime: null,
|
|
||||||
fault: null,
|
|
||||||
};
|
|
||||||
for (const cc of c.children) {
|
|
||||||
switch (cc.localName) {
|
|
||||||
case "UUID":
|
|
||||||
res.uuid = cc.text;
|
|
||||||
break;
|
|
||||||
case "DeploymentUnitRef":
|
|
||||||
res.deploymentUnitRef = cc.text;
|
|
||||||
break;
|
|
||||||
case "Version":
|
|
||||||
res.version = cc.text;
|
|
||||||
break;
|
|
||||||
case "CurrentState":
|
|
||||||
res.currentState = cc.text;
|
|
||||||
break;
|
|
||||||
case "Resolved":
|
|
||||||
res.resolved = parseBool(cc.text);
|
|
||||||
break;
|
|
||||||
case "ExecutionUnitRefList":
|
|
||||||
res.executionUnitRefList = cc.text;
|
|
||||||
break;
|
|
||||||
case "StartTime":
|
|
||||||
if (c.text) res.startTime = Date.parse(c.text);
|
|
||||||
break;
|
|
||||||
case "CompleteTime":
|
|
||||||
if (c.text) res.completeTime = Date.parse(c.text);
|
|
||||||
break;
|
|
||||||
case "Fault":
|
|
||||||
res.fault = {
|
|
||||||
faultCode: cc.children.find((n) => n.localName === "FaultCode")
|
|
||||||
.text,
|
|
||||||
faultString: cc.children.find(
|
|
||||||
(n) => n.localName === "FaultString"
|
|
||||||
).text,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results.push(res);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commandKey == null) {
|
|
||||||
warnings.push({
|
|
||||||
message: "Missing or invalid XML node",
|
|
||||||
element: "CommandKey",
|
|
||||||
});
|
|
||||||
commandKey = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!results) {
|
|
||||||
warnings.push({
|
|
||||||
message: "Missing or invalid XML node",
|
|
||||||
element: "Results",
|
|
||||||
});
|
|
||||||
results = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "DUStateChangeComplete",
|
|
||||||
commandKey: commandKey,
|
|
||||||
results: results,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function DUStateChangeCompleteResponse(): string {
|
|
||||||
return "<cwmp:DUStateChangeCompleteResponse></cwmp:DUStateChangeCompleteResponse>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function RequestDownload(xml: Element): RequestDownloadRequest {
|
function RequestDownload(xml: Element): RequestDownloadRequest {
|
||||||
return {
|
return {
|
||||||
name: "RequestDownload",
|
name: "RequestDownload",
|
||||||
@@ -1040,9 +942,6 @@ export function request(
|
|||||||
case "TransferComplete":
|
case "TransferComplete":
|
||||||
rpc.cpeRequest = TransferComplete(methodElement);
|
rpc.cpeRequest = TransferComplete(methodElement);
|
||||||
break;
|
break;
|
||||||
case "DUStateChangeComplete":
|
|
||||||
rpc.cpeRequest = DUStateChangeComplete(methodElement);
|
|
||||||
break;
|
|
||||||
case "RequestDownload":
|
case "RequestDownload":
|
||||||
rpc.cpeRequest = RequestDownload(methodElement);
|
rpc.cpeRequest = RequestDownload(methodElement);
|
||||||
break;
|
break;
|
||||||
@@ -1076,8 +975,8 @@ export function request(
|
|||||||
case "DownloadResponse":
|
case "DownloadResponse":
|
||||||
rpc.cpeResponse = DownloadResponse(methodElement);
|
rpc.cpeResponse = DownloadResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
case "ChangeDUStateResponse":
|
case "UploadResponse":
|
||||||
rpc.cpeResponse = ChangeDUStateResponse();
|
rpc.cpeResponse = UploadResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
case "Fault":
|
case "Fault":
|
||||||
rpc.cpeFault = fault(methodElement);
|
rpc.cpeFault = fault(methodElement);
|
||||||
@@ -1132,9 +1031,6 @@ export function response(rpc: {
|
|||||||
case "TransferCompleteResponse":
|
case "TransferCompleteResponse":
|
||||||
body = TransferCompleteResponse();
|
body = TransferCompleteResponse();
|
||||||
break;
|
break;
|
||||||
case "DUStateChangeCompleteResponse":
|
|
||||||
body = DUStateChangeCompleteResponse();
|
|
||||||
break;
|
|
||||||
case "RequestDownloadResponse":
|
case "RequestDownloadResponse":
|
||||||
body = RequestDownloadResponse();
|
body = RequestDownloadResponse();
|
||||||
break;
|
break;
|
||||||
@@ -1177,8 +1073,8 @@ export function response(rpc: {
|
|||||||
case "Download":
|
case "Download":
|
||||||
body = Download(rpc.acsRequest);
|
body = Download(rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
case "ChangeDUState":
|
case "Upload":
|
||||||
body = ChangeDUState(rpc.acsRequest);
|
body = Upload(rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
+28
-61
@@ -121,10 +121,10 @@ export interface SyncState {
|
|||||||
downloadsToCreate: InstanceSet;
|
downloadsToCreate: InstanceSet;
|
||||||
downloadsValues: Map<Path, string | number>;
|
downloadsValues: Map<Path, string | number>;
|
||||||
downloadsDownload: Map<Path, number>;
|
downloadsDownload: Map<Path, number>;
|
||||||
duStatesToDelete: Set<Path>;
|
uploadsToDelete: Set<Path>;
|
||||||
duStatesToCreate: InstanceSet;
|
uploadsToCreate: InstanceSet;
|
||||||
duStatesValues: Map<Path, string | number>;
|
uploadsValues: Map<Path, string | number>;
|
||||||
duStatesChangeDUState: Map<Path, number>;
|
uploadsUpload: Map<Path, number>;
|
||||||
reboot: number;
|
reboot: number;
|
||||||
factoryReset: number;
|
factoryReset: number;
|
||||||
}
|
}
|
||||||
@@ -187,34 +187,20 @@ export interface Task {
|
|||||||
provisions?: (string | number | boolean)[][];
|
provisions?: (string | number | boolean)[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OperationBase {
|
export interface Operation {
|
||||||
name: string;
|
name: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
provisions: string[][];
|
provisions: string[][];
|
||||||
channels: { [channel: string]: number };
|
channels: { [channel: string]: number };
|
||||||
retries: { [channel: string]: number };
|
retries: { [channel: string]: number };
|
||||||
args: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DownloadOperation extends OperationBase {
|
|
||||||
name: "Download";
|
|
||||||
args: {
|
args: {
|
||||||
instance: string;
|
instance: string;
|
||||||
fileType: string;
|
fileType: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
targetFileName: string;
|
targetFileName?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeDUStateOperation extends OperationBase {
|
|
||||||
name: "ChangeDUState";
|
|
||||||
args: {
|
|
||||||
operations: ChangeDUState["operations"];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Operation = DownloadOperation | ChangeDUStateOperation;
|
|
||||||
|
|
||||||
export type AcsRequest =
|
export type AcsRequest =
|
||||||
| GetParameterNames
|
| GetParameterNames
|
||||||
| GetParameterValues
|
| GetParameterValues
|
||||||
@@ -226,7 +212,7 @@ export type AcsRequest =
|
|||||||
| FactoryReset
|
| FactoryReset
|
||||||
| Reboot
|
| Reboot
|
||||||
| Download
|
| Download
|
||||||
| ChangeDUState;
|
| Upload;
|
||||||
|
|
||||||
export interface GetParameterNames {
|
export interface GetParameterNames {
|
||||||
name: "GetParameterNames";
|
name: "GetParameterNames";
|
||||||
@@ -296,19 +282,18 @@ export interface Download {
|
|||||||
failureUrl?: string;
|
failureUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeDUState {
|
export interface Upload {
|
||||||
name: "ChangeDUState";
|
name: "Upload";
|
||||||
commandKey: string;
|
commandKey: string;
|
||||||
operations: {
|
instance: string;
|
||||||
operationType: "install" | "update" | "uninstall";
|
fileType: string;
|
||||||
instance: string;
|
fileName?: string;
|
||||||
url: string;
|
url?: string;
|
||||||
uuid: string;
|
username?: string;
|
||||||
version: string;
|
password?: string;
|
||||||
executionEnvRef: string;
|
delaySecods?: number;
|
||||||
username: string;
|
successUrl?: string;
|
||||||
password: string;
|
failureUrl?: string;
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpvFault {
|
export interface SpvFault {
|
||||||
@@ -340,7 +325,7 @@ export type CpeResponse =
|
|||||||
| RebootResponse
|
| RebootResponse
|
||||||
| FactoryResetResponse
|
| FactoryResetResponse
|
||||||
| DownloadResponse
|
| DownloadResponse
|
||||||
| ChangeDUStateResponse;
|
| UploadResponse;
|
||||||
|
|
||||||
export interface GetParameterNamesResponse {
|
export interface GetParameterNamesResponse {
|
||||||
name: "GetParameterNamesResponse";
|
name: "GetParameterNamesResponse";
|
||||||
@@ -377,10 +362,6 @@ export interface DeleteObjectResponse {
|
|||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeDUStateResponse {
|
|
||||||
name: "ChangeDUStateResponse";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RebootResponse {
|
export interface RebootResponse {
|
||||||
name: "RebootResponse";
|
name: "RebootResponse";
|
||||||
}
|
}
|
||||||
@@ -396,12 +377,18 @@ export interface DownloadResponse {
|
|||||||
completeTime?: number;
|
completeTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadResponse {
|
||||||
|
name: "UploadResponse";
|
||||||
|
status: number;
|
||||||
|
startTime?: number;
|
||||||
|
completeTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type CpeRequest =
|
export type CpeRequest =
|
||||||
| InformRequest
|
| InformRequest
|
||||||
| TransferCompleteRequest
|
| TransferCompleteRequest
|
||||||
| GetRPCMethodsRequest
|
| GetRPCMethodsRequest
|
||||||
| RequestDownloadRequest
|
| RequestDownloadRequest;
|
||||||
| DUStateChangeCompleteRequest;
|
|
||||||
|
|
||||||
export interface InformRequest {
|
export interface InformRequest {
|
||||||
name: "Inform";
|
name: "Inform";
|
||||||
@@ -424,22 +411,6 @@ export interface TransferCompleteRequest {
|
|||||||
completeTime?: number;
|
completeTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DUStateChangeCompleteRequest {
|
|
||||||
name: "DUStateChangeComplete";
|
|
||||||
commandKey: string;
|
|
||||||
results: {
|
|
||||||
uuid: string;
|
|
||||||
deploymentUnitRef: string;
|
|
||||||
version: string;
|
|
||||||
currentState: string;
|
|
||||||
resolved: boolean;
|
|
||||||
executionUnitRefList: string;
|
|
||||||
fault?: { faultCode: string; faultString: string };
|
|
||||||
startTime?: number;
|
|
||||||
completeTime?: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetRPCMethodsRequest {
|
export interface GetRPCMethodsRequest {
|
||||||
name: "GetRPCMethods";
|
name: "GetRPCMethods";
|
||||||
}
|
}
|
||||||
@@ -453,8 +424,7 @@ export type AcsResponse =
|
|||||||
| InformResponse
|
| InformResponse
|
||||||
| GetRPCMethodsResponse
|
| GetRPCMethodsResponse
|
||||||
| TransferCompleteResponse
|
| TransferCompleteResponse
|
||||||
| RequestDownloadResponse
|
| RequestDownloadResponse;
|
||||||
| DUStateChangeCompleteResponse;
|
|
||||||
|
|
||||||
export interface InformResponse {
|
export interface InformResponse {
|
||||||
name: "InformResponse";
|
name: "InformResponse";
|
||||||
@@ -472,9 +442,6 @@ export interface TransferCompleteResponse {
|
|||||||
export interface RequestDownloadResponse {
|
export interface RequestDownloadResponse {
|
||||||
name: "RequestDownloadResponse";
|
name: "RequestDownloadResponse";
|
||||||
}
|
}
|
||||||
export interface DUStateChangeCompleteResponse {
|
|
||||||
name: "DUStateChangeCompleteResponse";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
projection?: any;
|
projection?: any;
|
||||||
|
|||||||
@@ -59,10 +59,8 @@ const getAuthorizer = memoize(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
koa.on("error", async (err, ctx) => {
|
koa.on("error", async (err) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Ignored errors resulting from aborted requests
|
|
||||||
if (ctx && ctx.req.aborted) return;
|
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -247,7 +245,7 @@ router.get("/", async (ctx) => {
|
|||||||
)};
|
)};
|
||||||
window.permissionSets = ${JSON.stringify(permissionSets)};
|
window.permissionSets = ${JSON.stringify(permissionSets)};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="app.js"></script>${wizard}
|
<script src="app.js"></script>${wizard}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
@@ -261,11 +259,6 @@ koa.use(
|
|||||||
deflate: {
|
deflate: {
|
||||||
flush: constants.Z_SYNC_FLUSH,
|
flush: constants.Z_SYNC_FLUSH,
|
||||||
},
|
},
|
||||||
br: {
|
|
||||||
params: {
|
|
||||||
[constants.BROTLI_PARAM_QUALITY]: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export async function deleteResource(
|
|||||||
await db.deleteFile(id);
|
await db.deleteFile(id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "uploads":
|
||||||
|
await db.deleteUpload(id);
|
||||||
|
break;
|
||||||
|
|
||||||
case "faults":
|
case "faults":
|
||||||
await deleteFault(id);
|
await deleteFault(id);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+59
-79
@@ -32,7 +32,6 @@ import Authorizer from "../common/authorizer";
|
|||||||
import { ping } from "../ping";
|
import { ping } from "../ping";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import { decodeTag } from "../common";
|
import { decodeTag } from "../common";
|
||||||
import { stringify as yamlStringify } from "../common/yaml";
|
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
export default router;
|
export default router;
|
||||||
@@ -50,6 +49,7 @@ const RESOURCE_IDS = {
|
|||||||
presets: "_id",
|
presets: "_id",
|
||||||
provisions: "_id",
|
provisions: "_id",
|
||||||
files: "_id",
|
files: "_id",
|
||||||
|
uploads: "_id",
|
||||||
virtualParameters: "_id",
|
virtualParameters: "_id",
|
||||||
config: "_id",
|
config: "_id",
|
||||||
permissions: "_id",
|
permissions: "_id",
|
||||||
@@ -63,6 +63,7 @@ const resources = {
|
|||||||
presets: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
presets: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||||
provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||||
files: 0 | RESOURCE_DELETE,
|
files: 0 | RESOURCE_DELETE,
|
||||||
|
uploads: 0 | RESOURCE_DELETE,
|
||||||
virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||||
config: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
config: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||||
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||||
@@ -71,10 +72,6 @@ const resources = {
|
|||||||
tasks: 0,
|
tasks: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function singleParam(p: string | string[]): string {
|
|
||||||
return Array.isArray(p) ? p[p.length - 1] : p;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get(`/devices/:id.csv`, async (ctx) => {
|
router.get(`/devices/:id.csv`, async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
@@ -111,28 +108,19 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
|||||||
|
|
||||||
for (const k of Object.keys(res[0]).sort()) {
|
for (const k of Object.keys(res[0]).sort()) {
|
||||||
const p = res[0][k];
|
const p = res[0][k];
|
||||||
let value = "";
|
|
||||||
let type = "";
|
|
||||||
if (p.value) {
|
|
||||||
value = p.value[0];
|
|
||||||
type = p.value[1];
|
|
||||||
if (type === "xsd:dateTime" && typeof value === "number")
|
|
||||||
value = new Date(value).toJSON();
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
k,
|
k,
|
||||||
p.object,
|
p.object,
|
||||||
new Date(p.objectTimestamp).toJSON(),
|
p.objectTimestamp,
|
||||||
p.writable,
|
p.writable,
|
||||||
new Date(p.writableTimestamp).toJSON(),
|
p.writableTimestamp,
|
||||||
`"${value.toString().replace(/"/g, '""')}"`,
|
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
||||||
type,
|
p.value != null ? p.value[1] : "",
|
||||||
new Date(p.valueTimestamp).toJSON(),
|
p.valueTimestamp,
|
||||||
p.notification,
|
p.notification,
|
||||||
new Date(p.notificationTimestamp).toJSON(),
|
p.notificationTimestamp,
|
||||||
p.accessList ? p.accessList.join(", ") : "",
|
p.accessList ? p.accessList.join(", ") : "",
|
||||||
new Date(p.accessListTimestamp).toJSON(),
|
p.accessListTimestamp,
|
||||||
];
|
];
|
||||||
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
|
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
|
||||||
}
|
}
|
||||||
@@ -145,7 +133,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
let filter: Expression = authorizer.getFilter(resource, 1);
|
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Count ${resource}`,
|
message: `Count ${resource}`,
|
||||||
@@ -182,13 +170,13 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: QueryOptions = {};
|
const options: QueryOptions = {};
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
options.sort = JSON.parse(singleParam(ctx.request.query.sort));
|
options.sort = JSON.parse(ctx.request.query.sort);
|
||||||
if (ctx.request.query.projection) {
|
if (ctx.request.query.projection) {
|
||||||
options.projection = singleParam(ctx.request.query.projection)
|
options.projection = ctx.request.query.projection
|
||||||
.split(",")
|
.split(",")
|
||||||
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
||||||
}
|
}
|
||||||
@@ -236,11 +224,11 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: QueryOptions = { projection: {} };
|
const options: QueryOptions = { projection: {} };
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
options.sort = JSON.parse(singleParam(ctx.request.query.sort));
|
options.sort = JSON.parse(ctx.request.query.sort);
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Query ${resource} (CSV)`,
|
message: `Query ${resource} (CSV)`,
|
||||||
@@ -257,7 +245,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: Record<string, Expression> = JSON.parse(
|
const columns: Record<string, Expression> = JSON.parse(
|
||||||
singleParam(ctx.request.query.columns)
|
ctx.request.query.columns
|
||||||
);
|
);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
@@ -299,23 +287,11 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
return tags.join(", ");
|
return tags.join(", ");
|
||||||
}
|
}
|
||||||
if (e === exp) {
|
|
||||||
const p = obj[e[1]];
|
|
||||||
if (
|
|
||||||
p &&
|
|
||||||
p.value &&
|
|
||||||
p.value[1] === "xsd:dateTime" &&
|
|
||||||
typeof p.value[0] === "number"
|
|
||||||
)
|
|
||||||
return new Date(p.value[0]).toJSON();
|
|
||||||
}
|
|
||||||
} else if (resource === "faults") {
|
|
||||||
if (e[1] === "detail") return yamlStringify(obj["detail"]);
|
|
||||||
}
|
}
|
||||||
} else if (e[0] === "FUNC") {
|
} else if (e[0] === "FUNC") {
|
||||||
if (e[1] === "DATE_STRING") {
|
if (e[1] === "DATE_STRING") {
|
||||||
if (e[2] && !Array.isArray(e[2]))
|
if (e[2] && !Array.isArray(e[2]))
|
||||||
return new Date(e[2]).toJSON();
|
return new Date(e[2]).toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +363,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (flags & RESOURCE_DELETE) {
|
if (flags & RESOURCE_DELETE) {
|
||||||
router.delete(`/${resource}/:id`, async (ctx) => {
|
router.delete(`/${resource}/:id(.+)`, async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: `Delete ${resource}`,
|
message: `Delete ${resource}`,
|
||||||
@@ -461,36 +437,6 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get("/blob/files/:id", async (ctx) => {
|
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
|
||||||
const resource = "files";
|
|
||||||
const id = ctx.params.id;
|
|
||||||
|
|
||||||
const log = {
|
|
||||||
message: `Download ${resource}`,
|
|
||||||
context: ctx,
|
|
||||||
id: id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter(resource, 2), [
|
|
||||||
"=",
|
|
||||||
["PARAM", RESOURCE_IDS[resource]],
|
|
||||||
ctx.params.id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!authorizer.hasAccess(resource, 2)) {
|
|
||||||
logUnauthorizedWarning(log);
|
|
||||||
return void (ctx.status = 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = await db.count(resource, filter);
|
|
||||||
if (!count) return void (ctx.status = 404);
|
|
||||||
|
|
||||||
logger.accessInfo(log);
|
|
||||||
ctx.body = db.downloadFile(id);
|
|
||||||
ctx.attachment(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put("/files/:id", async (ctx) => {
|
router.put("/files/:id", async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const resource = "files";
|
const resource = "files";
|
||||||
@@ -509,11 +455,10 @@ router.put("/files/:id", async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
fileType: singleParam(ctx.request.headers["metadata.filetype"]) || "",
|
fileType: ctx.request.headers["metadata.filetype"] || "",
|
||||||
oui: singleParam(ctx.request.headers["metadata.oui"]) || "",
|
oui: ctx.request.headers["metadata.oui"] || "",
|
||||||
productClass:
|
productClass: ctx.request.headers["metadata.productclass"] || "",
|
||||||
singleParam(ctx.request.headers["metadata.productclass"]) || "",
|
version: ctx.request.headers["metadata.version"] || "",
|
||||||
version: singleParam(ctx.request.headers["metadata.version"]) || "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = authorizer.getValidator(resource, metadata);
|
const validate = authorizer.getValidator(resource, metadata);
|
||||||
@@ -535,6 +480,41 @@ router.put("/files/:id", async (ctx) => {
|
|||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/uploads/blob/:id(.+)", async (ctx) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
|
const resource = "uploads";
|
||||||
|
const id = ctx.params.id;
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
message: `Download ${resource} blob`,
|
||||||
|
context: ctx,
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
1;
|
||||||
|
|
||||||
|
const filter = and(authorizer.getFilter(resource, 2), [
|
||||||
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS[resource]],
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void (ctx.status = 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db.query(resource, filter);
|
||||||
|
if (!res[0]) return void (ctx.status = 404);
|
||||||
|
|
||||||
|
ctx.type = "application/octet-stream";
|
||||||
|
ctx.attachment(id.replace(/\//g, "_"));
|
||||||
|
ctx.body = new stream.PassThrough();
|
||||||
|
|
||||||
|
const downloadStream = await db.getUploadBlob(id);
|
||||||
|
downloadStream.pipe(ctx.body);
|
||||||
|
logger.accessInfo(log);
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/devices/:id/tasks", async (ctx) => {
|
router.post("/devices/:id/tasks", async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
@@ -653,13 +633,13 @@ router.post("/devices/:id/tags", async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/ping/:host", async (ctx) => {
|
router.get("/ping/:host", async (ctx) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
ping(ctx.params.host, (err, parsed) => {
|
ping(ctx.params.host, (err, parsed) => {
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
ctx.body = parsed;
|
ctx.body = parsed;
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 500;
|
ctx.status = 500;
|
||||||
ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error";
|
ctx.body = `${err.name}: ${err.message}`;
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-39
@@ -28,6 +28,7 @@ import { minimize } from "../common/boolean-expression";
|
|||||||
|
|
||||||
const RESOURCE_COLLECTION = {
|
const RESOURCE_COLLECTION = {
|
||||||
files: "fs.files",
|
files: "fs.files",
|
||||||
|
uploads: "uploads.files",
|
||||||
};
|
};
|
||||||
|
|
||||||
let db: Db;
|
let db: Db;
|
||||||
@@ -36,23 +37,12 @@ onConnect(async (_db) => {
|
|||||||
db = _db;
|
db = _db;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function query(
|
|
||||||
resource: string,
|
|
||||||
filter: Expression,
|
|
||||||
options?: QueryOptions
|
|
||||||
): Promise<any[]>;
|
|
||||||
export function query(
|
|
||||||
resource: string,
|
|
||||||
filter: Expression,
|
|
||||||
options: QueryOptions,
|
|
||||||
callback: (doc: any) => void
|
|
||||||
): Promise<void>;
|
|
||||||
export function query(
|
export function query(
|
||||||
resource: string,
|
resource: string,
|
||||||
filter: Expression,
|
filter: Expression,
|
||||||
options?: QueryOptions,
|
options?: QueryOptions,
|
||||||
callback?: (doc: any) => void
|
callback?: (doc: any) => void
|
||||||
): Promise<void | any[]> {
|
): Promise<any[]> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
let q;
|
let q;
|
||||||
filter = expression.evaluate(filter, null, Date.now());
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
@@ -123,6 +113,8 @@ export function query(
|
|||||||
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
|
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
|
||||||
else if (resource === "files")
|
else if (resource === "files")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
|
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
|
||||||
|
else if (resource === "uploads")
|
||||||
|
docs = docs.map((d) => mongodbFunctions.flattenUpload(d));
|
||||||
return resolve(docs);
|
return resolve(docs);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -137,6 +129,8 @@ export function query(
|
|||||||
doc = mongodbFunctions.flattenPreset(doc);
|
doc = mongodbFunctions.flattenPreset(doc);
|
||||||
else if (resource === "files")
|
else if (resource === "files")
|
||||||
doc = mongodbFunctions.flattenFile(doc);
|
doc = mongodbFunctions.flattenFile(doc);
|
||||||
|
else if (resource === "uploads")
|
||||||
|
doc = mongodbFunctions.flattenUpload(doc);
|
||||||
callback(doc);
|
callback(doc);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -237,17 +231,9 @@ export function putProvision(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!object.script) object.script = "";
|
if (!object.script) object.script = "";
|
||||||
try {
|
try {
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||||
filename: id,
|
} catch (error) {
|
||||||
lineOffset: -1,
|
return Promise.reject(error);
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err.stack && err.stack.startsWith(`${id}:`)) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
}
|
||||||
return putResource("provisions", id, object);
|
return putResource("provisions", id, object);
|
||||||
}
|
}
|
||||||
@@ -262,17 +248,9 @@ export function putVirtualParameter(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!object.script) object.script = "";
|
if (!object.script) object.script = "";
|
||||||
try {
|
try {
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||||
filename: id,
|
} catch (error) {
|
||||||
lineOffset: -1,
|
return Promise.reject(error);
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err.stack && err.stack.startsWith(`${id}:`)) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
}
|
||||||
return putResource("virtualParameters", id, object);
|
return putResource("virtualParameters", id, object);
|
||||||
}
|
}
|
||||||
@@ -326,11 +304,6 @@ export function deleteUser(id: string): Promise<void> {
|
|||||||
return deleteResource("users", id);
|
return deleteResource("users", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadFile(filename: string): Readable {
|
|
||||||
const bucket = new GridFSBucket(db);
|
|
||||||
return bucket.openDownloadStreamByName(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function putFile(
|
export function putFile(
|
||||||
filename: string,
|
filename: string,
|
||||||
metadata: Record<string, string>,
|
metadata: Record<string, string>,
|
||||||
@@ -358,6 +331,16 @@ export function deleteFile(filename: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteUpload(filename: string): Promise<void> {
|
||||||
|
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
|
||||||
|
await bucket.delete(filename as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUploadBlob(filename: string): Promise<Readable> {
|
||||||
|
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
|
||||||
|
return bucket.openDownloadStreamByName(filename);
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteFault(id: string): Promise<void> {
|
export function deleteFault(id: string): Promise<void> {
|
||||||
return deleteResource("faults", id);
|
return deleteResource("faults", id);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1697
-4956
File diff suppressed because it is too large.
Load diff
+37
-33
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "genieacs",
|
"name": "genieacs",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -14,69 +14,73 @@
|
|||||||
],
|
],
|
||||||
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"preferGlobal": true,
|
||||||
"directories": {
|
"directories": {
|
||||||
"bin": "bin"
|
"bin": "bin",
|
||||||
|
"tools": "tools"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"espresso-iisojs": "^1.0.3",
|
"espresso-iisojs": "^1.0.3",
|
||||||
"iconv-lite": "^0.6.2",
|
"iconv-lite": "^0.6.2",
|
||||||
"ipaddr.js": "^2.0.0",
|
"ipaddr.js": "^2.0.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa": "^2.13.1",
|
"koa": "^2.13.0",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
"koa-compress": "^5.0.1",
|
"koa-compress": "^5.0.1",
|
||||||
"koa-jwt": "^4.0.0",
|
"koa-jwt": "^4.0.0",
|
||||||
"koa-router": "^10.0.0",
|
"koa-router": "^9.4.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"later": "^1.2.0",
|
"later": "^1.2.0",
|
||||||
"mongodb": "^3.6.3",
|
"mongodb": "^3.6.2",
|
||||||
"parsimmon": "^1.16.0",
|
"parsimmon": "^1.16.0",
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5",
|
||||||
|
"yaml": "^1.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^17.1.0",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^11.2.0",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@rollup/plugin-typescript": "^8.2.0",
|
"@rollup/plugin-typescript": "^6.0.0",
|
||||||
"@types/codemirror": "0.0.108",
|
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
"@types/koa": "^2.13.1",
|
"@types/koa": "^2.11.6",
|
||||||
"@types/koa-compress": "^4.0.1",
|
"@types/koa-compress": "^4.0.1",
|
||||||
"@types/koa-router": "^7.4.1",
|
"@types/koa-router": "^7.4.1",
|
||||||
"@types/koa-static": "^4.0.1",
|
"@types/koa-static": "^4.0.1",
|
||||||
"@types/mithril": "^2.0.6",
|
"@types/mithril": "^2.0.5",
|
||||||
"@types/mongodb": "^3.6.9",
|
"@types/mongodb": "^3.5.31",
|
||||||
"@types/node": "^14.14.34",
|
"@types/node": "^14.14.5",
|
||||||
"@types/parsimmon": "^1.10.6",
|
"@types/parsimmon": "^1.10.3",
|
||||||
"@types/seedrandom": "^3.0.0",
|
"@types/seedrandom": "^2.4.28",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
"@types/svgo": "^1.3.3",
|
||||||
"@typescript-eslint/parser": "^4.17.0",
|
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||||
"ava": "^3.15.0",
|
"@typescript-eslint/parser": "^4.6.0",
|
||||||
"codemirror": "^5.59.4",
|
"ava": "^3.13.0",
|
||||||
|
"codemirror": "^5.58.2",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"eslint": "^7.22.0",
|
"eslint": "^7.12.1",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^6.14.0",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
|
"jsbi": "^3.1.4",
|
||||||
"mithril": "^2.0.4",
|
"mithril": "^2.0.4",
|
||||||
"postcss": "^8.2.8",
|
"postcss": "^8.1.4",
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^13.0.0",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.1.2",
|
||||||
"rollup": "^2.41.2",
|
"rollup": "^2.32.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"svgo": "^2.2.2",
|
"source-map-loader": "^1.1.1",
|
||||||
"ts-node": "^9.1.1",
|
"svgo": "^1.3.2",
|
||||||
"typescript": "^4.2.3",
|
"ts-node": "^9.0.0",
|
||||||
"yaml": "^1.10.0"
|
"typescript": "^4.0.5",
|
||||||
|
"webpack": "^5.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.13.0"
|
"node": ">=12.13.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "ava --verbose --color",
|
"test": "ava --verbose --color",
|
||||||
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\"",
|
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/*.ts\"",
|
||||||
"lint-prettier": "prettier --prose-wrap always --write .",
|
"lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
||||||
"lint-tsc": "tsc --noEmit",
|
"lint-tsc": "tsc --noEmit",
|
||||||
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
|
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
|
||||||
"build": "node -r esm -r ts-node/register/transpile-only build/build"
|
"build": "node -r esm -r ts-node/register/transpile-only build/build"
|
||||||
|
|||||||
@@ -1,25 +1,6 @@
|
|||||||
import ava from "ava";
|
import ava from "ava";
|
||||||
import Path from "../lib/common/path";
|
import Path from "../lib/common/path";
|
||||||
|
|
||||||
ava("parse", (t) => {
|
|
||||||
t.throws(() => Path.parse("."));
|
|
||||||
t.throws(() => Path.parse("a "));
|
|
||||||
t.throws(() => Path.parse(".a"));
|
|
||||||
t.throws(() => Path.parse("a."));
|
|
||||||
t.throws(() => Path.parse("a..b"));
|
|
||||||
t.throws(() => Path.parse("b*"));
|
|
||||||
t.throws(() => Path.parse("*b"));
|
|
||||||
t.throws(() => Path.parse("a.b c.d"));
|
|
||||||
t.throws(() => Path.parse("a["));
|
|
||||||
t.throws(() => Path.parse("a[b"));
|
|
||||||
t.throws(() => Path.parse("a[b:"));
|
|
||||||
t.throws(() => Path.parse('a[b:"waef]'));
|
|
||||||
t.notThrows(() => Path.parse("[]"));
|
|
||||||
t.notThrows(() => Path.parse("[a:]"));
|
|
||||||
t.notThrows(() => Path.parse("*"));
|
|
||||||
t.notThrows(() => Path.parse(""));
|
|
||||||
});
|
|
||||||
|
|
||||||
ava("toString", (t) => {
|
ava("toString", (t) => {
|
||||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||||
|
|||||||
@@ -1,861 +0,0 @@
|
|||||||
[
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "Mark McGwire",
|
|
||||||
"hr": 65,
|
|
||||||
"avg": 0.278
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Sammy Sosa",
|
|
||||||
"hr": 63,
|
|
||||||
"avg": 0.288
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"top1": {
|
|
||||||
"key1": "scalar1"
|
|
||||||
},
|
|
||||||
"top2": {
|
|
||||||
"key2": "scalar2"
|
|
||||||
},
|
|
||||||
"top3": {
|
|
||||||
"scalar1": "scalar3"
|
|
||||||
},
|
|
||||||
"top4": {
|
|
||||||
"scalar2": "scalar4"
|
|
||||||
},
|
|
||||||
"top5": "scalar5",
|
|
||||||
"top6": {
|
|
||||||
"key6": "scalar6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text",
|
|
||||||
["a", "b", 42, "d"],
|
|
||||||
{
|
|
||||||
"a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe",
|
|
||||||
"?foo": "safe question mark",
|
|
||||||
":foo": "safe colon",
|
|
||||||
"-foo": "safe dash",
|
|
||||||
"this is#not": "a comment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"": "a"
|
|
||||||
},
|
|
||||||
"foo",
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"foo": "key"
|
|
||||||
},
|
|
||||||
[1, -2, 33],
|
|
||||||
{
|
|
||||||
"plain": "a b\nc"
|
|
||||||
},
|
|
||||||
[["s1_i1", "s1_i2"], "s2"],
|
|
||||||
{
|
|
||||||
"First occurrence": "Foo",
|
|
||||||
"Second occurrence": "Foo",
|
|
||||||
"Override anchor": "Bar",
|
|
||||||
"Reuse anchor": "Bar"
|
|
||||||
},
|
|
||||||
"k:#foo &a !t s",
|
|
||||||
["a"],
|
|
||||||
{
|
|
||||||
"escaped slash": "a/b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"plain": "This unquoted scalar spans many lines.",
|
|
||||||
"quoted": "So does this quoted scalar.\n"
|
|
||||||
},
|
|
||||||
"here's to \"quotes\"",
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
"ab cd\nef\n\ngh\n",
|
|
||||||
["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"],
|
|
||||||
"foo: bar\": baz",
|
|
||||||
"plain\\value\\with\\backslashes",
|
|
||||||
{
|
|
||||||
"plain": "text lines",
|
|
||||||
"quoted": "text lines",
|
|
||||||
"block": "text\n \tlines\n"
|
|
||||||
},
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
"foo": "you",
|
|
||||||
"bar": "far"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sequence": ["entry", ["nested"]],
|
|
||||||
"mapping": {
|
|
||||||
"foo": "bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"literal": "some\ntext\n",
|
|
||||||
"folded": "some text\n"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"one": "two",
|
|
||||||
"three": "four"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"five": "six",
|
|
||||||
"seven": "eight"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"Folding": "Empty line\nas a line feed",
|
|
||||||
"Chomping": "Clipped empty lines\n"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
["one", "two"],
|
|
||||||
["three", "four"]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"explicit key": null,
|
|
||||||
"block key\n": ["one", "two"]
|
|
||||||
},
|
|
||||||
["foo"],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
["baz", "baz"]
|
|
||||||
],
|
|
||||||
"ab\n\n \n",
|
|
||||||
{
|
|
||||||
"foo: bar\\": "baz'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Not indented": {
|
|
||||||
"By one space": "By four\n spaces\n",
|
|
||||||
"Flow style": ["By two", "Also by two", "Still by two"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"\\//||\\/||\n// || ||__\n",
|
|
||||||
{
|
|
||||||
"foo": [
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
"key": "value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": null,
|
|
||||||
"b": null
|
|
||||||
},
|
|
||||||
"foo",
|
|
||||||
{
|
|
||||||
"a": "b",
|
|
||||||
"": "a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo\nbar:baz\tx \\$%^&*()x": 23,
|
|
||||||
"x\\ny:z\\tx $%^&*()x": 24
|
|
||||||
},
|
|
||||||
"Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n",
|
|
||||||
" foo\nbar\nbaz ",
|
|
||||||
{ "matches %": 20 },
|
|
||||||
[
|
|
||||||
"flow in block",
|
|
||||||
"Block scalar\n",
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"a": "b",
|
|
||||||
"c": 42,
|
|
||||||
"e": "f",
|
|
||||||
"g": "h",
|
|
||||||
"23": false
|
|
||||||
},
|
|
||||||
"ab",
|
|
||||||
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
|
||||||
{
|
|
||||||
"top1": {
|
|
||||||
"key1": "one"
|
|
||||||
},
|
|
||||||
"top2": {
|
|
||||||
"key2": "two"
|
|
||||||
},
|
|
||||||
"top3": {
|
|
||||||
"key3": "three"
|
|
||||||
},
|
|
||||||
"top4": {
|
|
||||||
"key4": "four"
|
|
||||||
},
|
|
||||||
"top5": {
|
|
||||||
"key5": "five"
|
|
||||||
},
|
|
||||||
"top6": "six",
|
|
||||||
"top7": "seven"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hr": ["Mark McGwire", "Sammy Sosa"],
|
|
||||||
"rbi": ["Sammy Sosa", "Ken Griffey"]
|
|
||||||
},
|
|
||||||
"\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n",
|
|
||||||
["word1", "word2"],
|
|
||||||
{
|
|
||||||
"a": null,
|
|
||||||
"b": null,
|
|
||||||
"c": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nested sequences": [[[[]]], [[{}]]],
|
|
||||||
"key1": [],
|
|
||||||
"key2": {}
|
|
||||||
},
|
|
||||||
"---word1 word2",
|
|
||||||
{
|
|
||||||
"implicit block key": [
|
|
||||||
{
|
|
||||||
"implicit flow key": "value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key ends with two colons::": "value"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"single line": null,
|
|
||||||
"a": "b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multi line": null,
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
"key": ["item1", "item2"]
|
|
||||||
},
|
|
||||||
[
|
|
||||||
"double quoted",
|
|
||||||
"single quoted",
|
|
||||||
"plain text",
|
|
||||||
["nested"],
|
|
||||||
{
|
|
||||||
"single": "pair"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
["unicode anchor"],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"key2": "value2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key3": "value3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"trimmed\n\n\nas space",
|
|
||||||
"Mark McGwire's year was crippled by a knee injury.\n",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"single line": null,
|
|
||||||
"a": "b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multi line": null,
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"a": {
|
|
||||||
"b": {
|
|
||||||
"c": "d"
|
|
||||||
},
|
|
||||||
"e": {
|
|
||||||
"f": "g"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"h": "i"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"bar": "baz"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"single line": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multi line": "value"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"single": "text",
|
|
||||||
"double": "text"
|
|
||||||
},
|
|
||||||
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"item": "Super Hoop",
|
|
||||||
"quantity": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"item": "Basketball",
|
|
||||||
"quantity": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"item": "Big Shoes",
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"a b c d\ne",
|
|
||||||
{
|
|
||||||
"a": ["b", ["c", "d"]]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"strip": "text",
|
|
||||||
"clip": "text\n",
|
|
||||||
"keep": "text\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": "b c",
|
|
||||||
"d": "e f"
|
|
||||||
},
|
|
||||||
["single multiline - sequence entry"],
|
|
||||||
{
|
|
||||||
"1": [2, 3],
|
|
||||||
"4": 5
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"bla\"keks": "foo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bla]keks": "foo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"folded text\n",
|
|
||||||
"foo",
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"adjacent": "value",
|
|
||||||
"readable": "value",
|
|
||||||
"empty": null
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"a": "b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"c": "d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"e": "f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g": "h"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"tab": "\tstring"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"foo bar": "baz"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"anchored": "value",
|
|
||||||
"alias": "value"
|
|
||||||
},
|
|
||||||
["explicit indent and chomp", "chomp and explicit indent"],
|
|
||||||
{
|
|
||||||
"a": ["b", "c"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
"::vector",
|
|
||||||
": - ()",
|
|
||||||
"Up, up, and away!",
|
|
||||||
-123,
|
|
||||||
"http://example.com/foo#bar",
|
|
||||||
[
|
|
||||||
"::vector",
|
|
||||||
": - ()",
|
|
||||||
"Up, up and away!",
|
|
||||||
-123,
|
|
||||||
"http://example.com/foo#bar"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"a": "b",
|
|
||||||
"seq": ["a"],
|
|
||||||
"c": "d"
|
|
||||||
},
|
|
||||||
["foo", "bar", 42],
|
|
||||||
"line1 # no comment line3\n",
|
|
||||||
"\n\nliteral\n \n\ntext\n",
|
|
||||||
{
|
|
||||||
"a": "b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"k": ["a", "b"]
|
|
||||||
},
|
|
||||||
"a b c d\ne",
|
|
||||||
"---word1 word2",
|
|
||||||
["a", 2, 4, "d"],
|
|
||||||
{
|
|
||||||
"a": [
|
|
||||||
"b",
|
|
||||||
"c",
|
|
||||||
{
|
|
||||||
"d": ["e", "f"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": " more indented\nregular\n",
|
|
||||||
"b": "\n\n more indented\nregular\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"strip": "# text",
|
|
||||||
"clip": "# text\n",
|
|
||||||
"keep": "# text\n\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"safe": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~",
|
|
||||||
"safe question mark": "?foo",
|
|
||||||
"safe colon": ":foo",
|
|
||||||
"safe dash": "-foo"
|
|
||||||
},
|
|
||||||
"line1 line2 line3\n",
|
|
||||||
["Mark McGwire", "Sammy Sosa", "Ken Griffey"],
|
|
||||||
["a"],
|
|
||||||
["a", ["b", "c"]],
|
|
||||||
{
|
|
||||||
"unicode": "Sosa did fine.☺",
|
|
||||||
"control": "\b1998\t1999\t2000\n",
|
|
||||||
"hex esc": "\r\n is \r\n",
|
|
||||||
"single": "\"Howdy!\" he cried.",
|
|
||||||
"quoted": " # Not a 'comment'.",
|
|
||||||
"tie-fighter": "|\\-*-/|"
|
|
||||||
},
|
|
||||||
[["-", "-"]],
|
|
||||||
"folded text\n",
|
|
||||||
{
|
|
||||||
"a": 13,
|
|
||||||
"1.5": "d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": 1,
|
|
||||||
"bar": 2,
|
|
||||||
"text": "a\n \nb\n\nc\n\nd\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"wanted": "love ♥ and peace ☮"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Mark McGwire",
|
|
||||||
"accomplishment": "Mark set a major league home run record in 1998.\n",
|
|
||||||
"stats": "65 Home Runs\n0.278 Batting Average\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "foo"
|
|
||||||
},
|
|
||||||
"1st non-empty\n2nd non-empty 3rd non-empty",
|
|
||||||
{
|
|
||||||
"quoted": "Quoted \t",
|
|
||||||
"block": "void main() {\n\tprintf(\"Hello, world!\\n\");\n}\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": "blue",
|
|
||||||
"bar": "arrr",
|
|
||||||
"baz": "jazz"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"Mark McGwire": 65
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sammy Sosa": 63
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Ken Griffy": 58
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"1": 2,
|
|
||||||
"3": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hr": ["Mark McGwire", "Sammy Sosa"],
|
|
||||||
"rbi": ["Sammy Sosa", "Ken Griffey"]
|
|
||||||
},
|
|
||||||
"k:#foo &a !t s",
|
|
||||||
{
|
|
||||||
"block sequence": [
|
|
||||||
"one",
|
|
||||||
{
|
|
||||||
"two": "three"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"First occurrence": "Value",
|
|
||||||
"Second occurrence": "Value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a true": "null d",
|
|
||||||
"e 42": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
["foo", "bar", 42],
|
|
||||||
"trimmed\n\n\nas space",
|
|
||||||
"scalar",
|
|
||||||
{
|
|
||||||
"strip": "",
|
|
||||||
"clip": "",
|
|
||||||
"keep": "\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": {
|
|
||||||
"bar": 1
|
|
||||||
},
|
|
||||||
"baz": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": 47,
|
|
||||||
"c": "d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"implicit block key": [
|
|
||||||
{
|
|
||||||
"implicit flow key": "value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
["a", "b", "c", "c", ""],
|
|
||||||
[
|
|
||||||
["a", "b", "c"],
|
|
||||||
{
|
|
||||||
"a": "b",
|
|
||||||
"c": "d",
|
|
||||||
"e": "f"
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"implicit block key": [
|
|
||||||
{
|
|
||||||
"implicit flow key": "value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": "ab\n\ncd\nef\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"literal": "value\n",
|
|
||||||
"folded": "value\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": [
|
|
||||||
"b",
|
|
||||||
"c",
|
|
||||||
{
|
|
||||||
"d": ["e", "f"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"literal\n\ttext\n",
|
|
||||||
"foo \n\n\t bar\n\nbaz\n",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"a": "b"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ab",
|
|
||||||
["plain", "double quoted", "single quoted", "block\n", "plain again"],
|
|
||||||
{
|
|
||||||
"a": " ",
|
|
||||||
"b": " ",
|
|
||||||
"c": " ",
|
|
||||||
"d": " ",
|
|
||||||
"e": "\n",
|
|
||||||
"f": "\n",
|
|
||||||
"g": "\n\n",
|
|
||||||
"h": "\n\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value with\ntabs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"": null
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"single line": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multi line": "value"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"folded to a space,\nto a line feed, or \t \tnon-content",
|
|
||||||
["literal\n", " folded\n", "keep\n\n", " strip"],
|
|
||||||
{
|
|
||||||
"key": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"],
|
|
||||||
"national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"]
|
|
||||||
},
|
|
||||||
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
|
||||||
{},
|
|
||||||
[
|
|
||||||
["a", "b"],
|
|
||||||
{
|
|
||||||
"a": "b"
|
|
||||||
},
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
"c"
|
|
||||||
],
|
|
||||||
"folded to a space,\nto a line feed, or \t \tnon-content",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"],
|
|
||||||
{
|
|
||||||
"top1": [
|
|
||||||
"item1",
|
|
||||||
{
|
|
||||||
"key2": "value2"
|
|
||||||
},
|
|
||||||
"item3"
|
|
||||||
],
|
|
||||||
"top2": "value2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": [42],
|
|
||||||
"bar": [44]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"23": "d",
|
|
||||||
"a": 4.2
|
|
||||||
},
|
|
||||||
"Document",
|
|
||||||
{
|
|
||||||
"Date": "2001-11-23 15:03:17 -5",
|
|
||||||
"User": "ed",
|
|
||||||
"Fatal": "Unknown variable \"bar\"",
|
|
||||||
"Stack": [
|
|
||||||
{
|
|
||||||
"file": "TopClass.py",
|
|
||||||
"line": 23,
|
|
||||||
"code": "x = MoreObject(\"345\\n\")\n"
|
|
||||||
},
|
|
||||||
{ "file": "MoreClass.py", "line": 58, "code": "foo = bar" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"plain key": "in-line value",
|
|
||||||
"": null,
|
|
||||||
"quoted key": ["entry"]
|
|
||||||
},
|
|
||||||
["12", 12, "12"],
|
|
||||||
{
|
|
||||||
"aaa": "bbb"
|
|
||||||
},
|
|
||||||
[":,"],
|
|
||||||
{
|
|
||||||
"sequence": ["one", "two"],
|
|
||||||
"mapping": {
|
|
||||||
"sky": "blue",
|
|
||||||
"sea": "green"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"seq": ["a", "b"]
|
|
||||||
},
|
|
||||||
"here's to \"quotes\"",
|
|
||||||
{
|
|
||||||
"hr": 65,
|
|
||||||
"avg": 0.278,
|
|
||||||
"rbi": 147
|
|
||||||
},
|
|
||||||
"\n\nliteral\n \n\ntext\n",
|
|
||||||
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
|
||||||
"literal\n\ttext\n",
|
|
||||||
{
|
|
||||||
"block mapping": {
|
|
||||||
"key": "value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
" foo\nbar\nbaz ",
|
|
||||||
"ab cd\nef\n\ngh\n",
|
|
||||||
"foo",
|
|
||||||
{
|
|
||||||
"top1": {
|
|
||||||
"key1": "one"
|
|
||||||
},
|
|
||||||
"top2": {
|
|
||||||
"key2": "two"
|
|
||||||
},
|
|
||||||
"top3": {
|
|
||||||
"key3": "three"
|
|
||||||
},
|
|
||||||
"top4": {
|
|
||||||
"key4": "four"
|
|
||||||
},
|
|
||||||
"top5": {
|
|
||||||
"key5": "five"
|
|
||||||
},
|
|
||||||
"top6": "six",
|
|
||||||
"top7": "seven"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"url": "http://example.org"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"sequence": ["one", "two"],
|
|
||||||
"mapping": {
|
|
||||||
"sky": "blue",
|
|
||||||
"sea": "green"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"invoice": 34843,
|
|
||||||
"date": "2001-01-23",
|
|
||||||
"bill-to": {
|
|
||||||
"given": "Chris",
|
|
||||||
"family": "Dumars",
|
|
||||||
"address": {
|
|
||||||
"lines": "458 Walkman Dr.\nSuite #292\n",
|
|
||||||
"city": "Royal Oak",
|
|
||||||
"state": "MI",
|
|
||||||
"postal": 48046
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ship-to": {
|
|
||||||
"given": "Chris",
|
|
||||||
"family": "Dumars",
|
|
||||||
"address": {
|
|
||||||
"lines": "458 Walkman Dr.\nSuite #292\n",
|
|
||||||
"city": "Royal Oak",
|
|
||||||
"state": "MI",
|
|
||||||
"postal": 48046
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"product": [
|
|
||||||
{
|
|
||||||
"sku": "BL394D",
|
|
||||||
"quantity": 4,
|
|
||||||
"description": "Basketball",
|
|
||||||
"price": 450
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"sku": "BL4438H",
|
|
||||||
"quantity": 1,
|
|
||||||
"description": "Super Hoop",
|
|
||||||
"price": 2392
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tax": 251.42,
|
|
||||||
"total": 4443.52,
|
|
||||||
"comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."
|
|
||||||
},
|
|
||||||
["a", "b", "a", "b"],
|
|
||||||
[
|
|
||||||
null,
|
|
||||||
"block node\n",
|
|
||||||
["one", "two"],
|
|
||||||
{
|
|
||||||
"one": "two"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"a": "scalar a",
|
|
||||||
"b": "scalar a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"foo": "",
|
|
||||||
"": "bar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value"
|
|
||||||
},
|
|
||||||
"scalar %YAML 1.2",
|
|
||||||
{
|
|
||||||
"Folding": "Empty line\nas a line feed",
|
|
||||||
"Chomping": "Clipped empty lines\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "value"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
["name", "hr", "avg"],
|
|
||||||
["Mark McGwire", 65, 0.278],
|
|
||||||
["Sammy Sosa", 63, 0.288]
|
|
||||||
],
|
|
||||||
{
|
|
||||||
"literal": "value\n",
|
|
||||||
"folded": "value\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Mark McGwire": {
|
|
||||||
"hr": 65,
|
|
||||||
"avg": 0.278
|
|
||||||
},
|
|
||||||
"Sammy Sosa": {
|
|
||||||
"hr": 63,
|
|
||||||
"avg": 0.288
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": "b",
|
|
||||||
"c": "d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": [[["value"]]]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"a": 1,
|
|
||||||
"b": null,
|
|
||||||
"c": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "outgoing HTTP response",
|
|
||||||
"timestamp": "2021-01-21T18:44:57.116Z",
|
|
||||||
"remoteAddress": "127.0.0.1",
|
|
||||||
"deviceId": "202BC1-BM632w-000000",
|
|
||||||
"connection": "2021-01-21T18:44:57.105Z",
|
|
||||||
"statusCode": 200,
|
|
||||||
"headers": {
|
|
||||||
"content-length": 528,
|
|
||||||
"server": "GenieACS/1.2.3+20210121000910",
|
|
||||||
"soapserver": "GenieACS/1.2.3+20210121000910",
|
|
||||||
"content-type": "text/xml; charset=\"utf-8\"",
|
|
||||||
"set-cookie": "session=ea31d97ccf6832fb"
|
|
||||||
},
|
|
||||||
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soap-env:Envelope xmlns:soap-enc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:cwmp=\"urn:dslforum-org:cwmp-1-0\"><soap-env:Header><cwmp:ID soap-env:mustUnderstand=\"1\">wdsx50vq</cwmp:ID></soap-env:Header><soap-env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap-env:Body></soap-env:Envelope>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import ava from "ava";
|
|
||||||
import * as yaml from "yaml";
|
|
||||||
import { stringify } from "../lib/common/yaml";
|
|
||||||
|
|
||||||
import testCases from "./yaml-tests.json";
|
|
||||||
|
|
||||||
ava("stringify", (t) => {
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
let str = stringify(testCase);
|
|
||||||
if (str.startsWith(">2")) str = ">3" + str.slice(2);
|
|
||||||
if (str.startsWith("|2")) str = "|3" + str.slice(2);
|
|
||||||
t.deepEqual(yaml.parse(yaml.stringify(testCase)), yaml.parse(str));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { connect, devicesCollection } from "../lib/db";
|
||||||
|
|
||||||
|
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
|
||||||
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
|
|
||||||
|
const dataModel = {};
|
||||||
|
|
||||||
|
function recursive(object, p = ""): void {
|
||||||
|
for (const key in object) {
|
||||||
|
if (key[0] === "_" || !object[key]) continue;
|
||||||
|
|
||||||
|
const isObject = object[key]["_object"];
|
||||||
|
const isValue = "_value" in object[key];
|
||||||
|
|
||||||
|
if ((isObject && isValue) || (!isObject && !isValue))
|
||||||
|
throw new Error("Unexpected object");
|
||||||
|
|
||||||
|
if (isValue) {
|
||||||
|
let value = object[key]["_value"];
|
||||||
|
if (typeof value === "boolean") value = +value;
|
||||||
|
|
||||||
|
const values = [object[key]["_writable"] || false, value.toString()];
|
||||||
|
if (object[key]["_type"]) values.push(object[key]["_type"]);
|
||||||
|
|
||||||
|
dataModel[`${p}${key}`] = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject) {
|
||||||
|
const values = [object[key]["_writable"] || false];
|
||||||
|
dataModel[`${p}${key}.`] = values;
|
||||||
|
recursive(object[key], `${p}${key}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect()
|
||||||
|
.then(() => {
|
||||||
|
devicesCollection.findOne(
|
||||||
|
{
|
||||||
|
_id: process.argv[2],
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
(err, device) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
if (!device) throw new Error("Device not found");
|
||||||
|
|
||||||
|
recursive(device);
|
||||||
|
|
||||||
|
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
process.stdout.write(`${outputFilename} saved\n`);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
+9
-6
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["esnext", "dom"],
|
"lib": [
|
||||||
|
"esnext", "dom"
|
||||||
|
],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
@@ -12,10 +14,11 @@
|
|||||||
"allowJs": true
|
"allowJs": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./bin/*.ts",
|
"./bin/*.ts",
|
||||||
"./lib/**/*",
|
"./lib/**/*",
|
||||||
"./ui/**/*",
|
"./ui/**/*",
|
||||||
"./test/**/*",
|
"./test/**/*",
|
||||||
"./build/**/*"
|
"./build/**/*",
|
||||||
|
"./tools/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -64,37 +64,50 @@ const adminPages = [
|
|||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
|
let lastRenderTimestamp = 0;
|
||||||
|
let pageVisitTimestamp = 0;
|
||||||
|
let fulfillTimeout = null;
|
||||||
|
|
||||||
|
function fulfill(): void {
|
||||||
|
clearTimeout(fulfillTimeout);
|
||||||
|
fulfillTimeout = setTimeout(() => {
|
||||||
|
store.fulfill(lastRenderTimestamp, pageVisitTimestamp, (updated) => {
|
||||||
|
if (updated) m.redraw();
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
function pagify(pageName, page): RouteResolver {
|
function pagify(pageName, page): RouteResolver {
|
||||||
const component: RouteResolver = {
|
const component: RouteResolver = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const lastRenderTimestamp = Date.now();
|
lastRenderTimestamp = Date.now();
|
||||||
let p;
|
let p;
|
||||||
if (state && state.error) p = m(errorPage.component, state);
|
if (state && state.error) p = m(errorPage.component, state);
|
||||||
else p = m(contextifyComponent(page.component), state);
|
else p = m(contextifyComponent(page.component), state);
|
||||||
|
fulfill();
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
attrs["page"] = pageName;
|
attrs["page"] = pageName;
|
||||||
attrs["oncreate"] = attrs["onupdate"] = () => {
|
|
||||||
store.fulfill(lastRenderTimestamp);
|
|
||||||
};
|
|
||||||
return m(layout, attrs, p);
|
return m(layout, attrs, p);
|
||||||
},
|
},
|
||||||
onmatch: null,
|
onmatch: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
component.onmatch = (args, requestedPath) => {
|
component.onmatch = (args, requestedPath) => {
|
||||||
store.setTimestamp(Date.now());
|
pageVisitTimestamp = Date.now();
|
||||||
if (!page.init) {
|
if (!page.init) {
|
||||||
state = null;
|
state = null;
|
||||||
|
fulfill();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
page
|
page
|
||||||
.init(args)
|
.init(args)
|
||||||
.then((st) => {
|
.then((st) => {
|
||||||
if (!st) return void m.route.set("/");
|
if (!st) return void m.route.set("/");
|
||||||
state = st;
|
state = st;
|
||||||
resolve();
|
resolve();
|
||||||
|
fulfill();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
||||||
|
|||||||
+14
-17
@@ -28,24 +28,21 @@ const component: ClosureComponent = (): Component => {
|
|||||||
name: vnode.attrs["id"],
|
name: vnode.attrs["id"],
|
||||||
value: vnode.attrs["value"],
|
value: vnode.attrs["value"],
|
||||||
oncreate: (_vnode) => {
|
oncreate: (_vnode) => {
|
||||||
const editor = codeMirror.fromTextArea(
|
const editor = codeMirror.fromTextArea(_vnode.dom, {
|
||||||
_vnode.dom as HTMLTextAreaElement,
|
mode: vnode.attrs["mode"],
|
||||||
{
|
lineNumbers: true,
|
||||||
mode: vnode.attrs["mode"],
|
readOnly: vnode.attrs["readOnly"],
|
||||||
lineNumbers: true,
|
extraKeys: {
|
||||||
readOnly: vnode.attrs["readOnly"],
|
"Ctrl-Enter": () => {
|
||||||
extraKeys: {
|
if (vnode.attrs["onSubmit"])
|
||||||
"Ctrl-Enter": () => {
|
vnode.attrs["onSubmit"](_vnode.dom);
|
||||||
if (vnode.attrs["onSubmit"])
|
|
||||||
vnode.attrs["onSubmit"](_vnode.dom);
|
|
||||||
},
|
|
||||||
"Cmd-Enter": () => {
|
|
||||||
if (vnode.attrs["onSubmit"])
|
|
||||||
vnode.attrs["onSubmit"](_vnode.dom);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
"Cmd-Enter": () => {
|
||||||
);
|
if (vnode.attrs["onSubmit"])
|
||||||
|
vnode.attrs["onSubmit"](_vnode.dom);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (vnode.attrs["onChange"]) {
|
if (vnode.attrs["onChange"]) {
|
||||||
editor.on("change", (e) => {
|
editor.on("change", (e) => {
|
||||||
|
|||||||
+5
-9
@@ -39,7 +39,7 @@ import tags from "./components/tags";
|
|||||||
import ping from "./components/ping";
|
import ping from "./components/ping";
|
||||||
import deviceLink from "./components/device-link";
|
import deviceLink from "./components/device-link";
|
||||||
import longTextComponent from "./long-text-component";
|
import longTextComponent from "./long-text-component";
|
||||||
import loading from "./components/loading";
|
import deviceUploads from "./components/device-uploads";
|
||||||
|
|
||||||
const comps = {
|
const comps = {
|
||||||
parameter,
|
parameter,
|
||||||
@@ -55,7 +55,7 @@ const comps = {
|
|||||||
ping,
|
ping,
|
||||||
"device-link": deviceLink,
|
"device-link": deviceLink,
|
||||||
"long-text": longTextComponent,
|
"long-text": longTextComponent,
|
||||||
loading: loading,
|
"device-uploads": deviceUploads,
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
||||||
@@ -139,13 +139,9 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
|||||||
const view = state.view;
|
const view = state.view;
|
||||||
state.view = function (vnode) {
|
state.view = function (vnode) {
|
||||||
const context = vnodeContext.get(vnode) || {};
|
const context = vnodeContext.get(vnode) || {};
|
||||||
try {
|
const res = Reflect.apply(view, this, [vnode]);
|
||||||
const res = Reflect.apply(view, this, [vnode]);
|
applyContext(res, context);
|
||||||
applyContext(res, context);
|
return res;
|
||||||
return res;
|
|
||||||
} catch (err) {
|
|
||||||
return m("p.error", { title: err.message }, "Error!");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
const limit = vnode.attrs["limit"] > 0 ? vnode.attrs["limit"] : 100;
|
|
||||||
|
|
||||||
const search = m("input", {
|
const search = m("input", {
|
||||||
type: "text",
|
type: "text",
|
||||||
placeholder: "Search parameters",
|
placeholder: "Search parameters",
|
||||||
@@ -88,7 +86,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
|
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
|
||||||
if (re && !re.test(str)) continue;
|
if (re && !re.test(str)) continue;
|
||||||
++c;
|
++c;
|
||||||
if (count < limit) filteredKeys.push(k);
|
if (count < 100) filteredKeys.push(k);
|
||||||
}
|
}
|
||||||
count += c;
|
count += c;
|
||||||
}
|
}
|
||||||
@@ -171,29 +169,25 @@ const component: ClosureComponent = (): Component => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
".all-parameters",
|
||||||
{ queries: [vnode.attrs["deviceQuery"]] },
|
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
"a.download-csv",
|
||||||
|
{
|
||||||
|
href: `api/devices/${encodeURIComponent(
|
||||||
|
device["DeviceID.ID"].value[0]
|
||||||
|
)}.csv`,
|
||||||
|
download: "",
|
||||||
|
style: "float: right;",
|
||||||
|
},
|
||||||
|
"Download"
|
||||||
|
),
|
||||||
|
search,
|
||||||
|
m(
|
||||||
|
".parameter-list",
|
||||||
|
m("table", m("tbody", rows)),
|
||||||
m(
|
m(
|
||||||
"a.download-csv",
|
"m",
|
||||||
{
|
`Displaying ${filteredKeys.length} out of ${count} parameters.`
|
||||||
href: `api/devices/${encodeURIComponent(
|
|
||||||
device["DeviceID.ID"].value[0]
|
|
||||||
)}.csv`,
|
|
||||||
download: "",
|
|
||||||
style: "float: right;",
|
|
||||||
},
|
|
||||||
"Download"
|
|
||||||
),
|
|
||||||
search,
|
|
||||||
m(
|
|
||||||
".parameter-list",
|
|
||||||
m("table", m("tbody", rows)),
|
|
||||||
m(
|
|
||||||
"m",
|
|
||||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { m } from "../components";
|
|||||||
import * as taskQueue from "../task-queue";
|
import * as taskQueue from "../task-queue";
|
||||||
import * as notifications from "../notifications";
|
import * as notifications from "../notifications";
|
||||||
import * as store from "../store";
|
import * as store from "../store";
|
||||||
|
import * as overlay from "../overlay";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
@@ -78,6 +79,30 @@ const component: ClosureComponent = (): Component => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Fetch files from device",
|
||||||
|
onclick: () => {
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
overlay.open(() => {
|
||||||
|
const dev = store.fetch("devices", [
|
||||||
|
"=",
|
||||||
|
["PARAM", "DeviceID.ID"],
|
||||||
|
deviceId,
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
m("h3", `Uploads from ${deviceId}`),
|
||||||
|
m("device-uploads", { device: dev.value[0] }),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Uploads"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.primary",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { m } from "../components";
|
|||||||
import * as store from "../store";
|
import * as store from "../store";
|
||||||
import * as notifications from "../notifications";
|
import * as notifications from "../notifications";
|
||||||
import { getIcon } from "../icons";
|
import { getIcon } from "../icons";
|
||||||
import { stringify } from "../../lib/common/yaml";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
@@ -39,7 +38,6 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"Channel",
|
"Channel",
|
||||||
"Code",
|
"Code",
|
||||||
"Message",
|
"Message",
|
||||||
"Detail",
|
|
||||||
"Retries",
|
"Retries",
|
||||||
"Timestamp",
|
"Timestamp",
|
||||||
].map((l) => m("th", l));
|
].map((l) => m("th", l));
|
||||||
@@ -50,9 +48,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
rows.push([
|
rows.push([
|
||||||
m("td", f["channel"]),
|
m("td", f["channel"]),
|
||||||
m("td", f["code"]),
|
m("td", f["code"]),
|
||||||
m("td", m("long-text", { text: f["message"] })),
|
m("td", f["message"]),
|
||||||
m("td", m("long-text", { text: stringify(f["detail"]) })),
|
|
||||||
|
|
||||||
m("td", f["retries"]),
|
m("td", f["retries"]),
|
||||||
m("td", new Date(f["timestamp"]).toLocaleString()),
|
m("td", new Date(f["timestamp"]).toLocaleString()),
|
||||||
m(
|
m(
|
||||||
@@ -67,12 +63,12 @@ const component: ClosureComponent = (): Component => {
|
|||||||
.deleteResource("faults", f["_id"])
|
.deleteResource("faults", f["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Fault deleted");
|
notifications.push("success", "Fault deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(Date.now(), Date.now());
|
||||||
m.redraw();
|
m.redraw();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(Date.now(), Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -95,11 +91,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m("table.table", thead, tbody);
|
||||||
"loading",
|
|
||||||
{ queries: [faults] },
|
|
||||||
m("table.table", thead, tbody)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2020 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as store from "../store";
|
||||||
|
import * as notifications from "../notifications";
|
||||||
|
import { getIcon } from "../icons";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: (vnode) => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
const files = store.fetch("uploads", [
|
||||||
|
"AND",
|
||||||
|
[">", ["PARAM", "_id"], `${deviceId}/`],
|
||||||
|
["<", ["PARAM", "_id"], `${deviceId}/zzzz`],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const uploads: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(device)) {
|
||||||
|
if (k.startsWith("Uploads.")) {
|
||||||
|
const parts = k.split(".");
|
||||||
|
if (parts.length !== 3) continue;
|
||||||
|
uploads[parts[1]] = uploads[parts[1]] || {};
|
||||||
|
uploads[parts[1]][parts[2]] = v["value"][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ["Filename", "Type", "Timestamp", "Status"].map((l) =>
|
||||||
|
m("th", l)
|
||||||
|
);
|
||||||
|
const thead = m("thead", m("tr", headers));
|
||||||
|
|
||||||
|
let minTime = 999999;
|
||||||
|
const timestamp = store.getTimestamp();
|
||||||
|
const rows = [];
|
||||||
|
const uploadsSorted = Object.values(uploads).sort(
|
||||||
|
(a, b) => a["Upload"] - b["Upload"]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const u of Object.values(uploadsSorted)) {
|
||||||
|
if (!u["Upload"]) continue;
|
||||||
|
const ready = u["LastUpload"] >= u["Upload"];
|
||||||
|
if (!ready) {
|
||||||
|
minTime = Math.min(minTime, u["Upload"] - timestamp);
|
||||||
|
rows.push([
|
||||||
|
m("td", u["FileName"]),
|
||||||
|
m("td", u["FileType"]),
|
||||||
|
m("td", new Date(u["Upload"]).toLocaleString()),
|
||||||
|
m("td", "Waiting for upload"),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
const filePath = `${deviceId}/${u["FileName"]}`;
|
||||||
|
if (!Array.isArray(files.value)) continue;
|
||||||
|
if (!files.value.some((f) => f["_id"] === filePath)) continue;
|
||||||
|
|
||||||
|
const deleteButton = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Delete file",
|
||||||
|
onclick: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
store
|
||||||
|
.deleteResource("uploads", filePath)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", "File deleted");
|
||||||
|
store.fulfill(Date.now(), Date.now());
|
||||||
|
m.redraw();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.fulfill(Date.now(), Date.now());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getIcon("delete-instance")
|
||||||
|
);
|
||||||
|
|
||||||
|
rows.push([
|
||||||
|
m("td", u["FileName"]),
|
||||||
|
m("td", u["FileType"]),
|
||||||
|
m("td", new Date(u["LastUpload"]).toLocaleString()),
|
||||||
|
m(
|
||||||
|
"td",
|
||||||
|
m(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
href: `api/uploads/blob/${encodeURIComponent(filePath)}`,
|
||||||
|
},
|
||||||
|
"Ready"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m("td", deleteButton),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minTime < 60000) {
|
||||||
|
setTimeout(() => {
|
||||||
|
store.fulfill(0, timestamp + 5000);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addButton = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Fetch a new file",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.stageUpload({
|
||||||
|
name: "upload",
|
||||||
|
devices: [deviceId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getIcon("add-instance")
|
||||||
|
);
|
||||||
|
|
||||||
|
const empty = !rows.length;
|
||||||
|
|
||||||
|
rows.push([m("td", { colspan: headers.length }), m("td", addButton)]);
|
||||||
|
|
||||||
|
const tbody = m(
|
||||||
|
"tbody",
|
||||||
|
rows.map((r) => m("tr", r)),
|
||||||
|
empty
|
||||||
|
? m("tr.empty", m("td", { colspan: headers.length }, "No uploads"))
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return m("table.table", thead, tbody);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2013-2021 GenieACS Inc.
|
|
||||||
*
|
|
||||||
* This file is part of GenieACS.
|
|
||||||
*
|
|
||||||
* GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* GenieACS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { VnodeDOM, ClosureComponent, Component } from "mithril";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
let overlay: HTMLElement;
|
|
||||||
let dom: Element;
|
|
||||||
let loading = false;
|
|
||||||
|
|
||||||
function apply(vnode: VnodeDOM): void {
|
|
||||||
if (!loading) {
|
|
||||||
if (overlay) overlay.parentElement.remove();
|
|
||||||
if (dom) dom.classList.remove("loading");
|
|
||||||
overlay = null;
|
|
||||||
dom = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dom && dom !== vnode.dom) dom.classList.remove("loading");
|
|
||||||
|
|
||||||
dom = vnode.dom;
|
|
||||||
dom.classList.add("loading");
|
|
||||||
|
|
||||||
if (!overlay) {
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
wrapper.style.position = "relative";
|
|
||||||
wrapper.style.pointerEvents = "none";
|
|
||||||
overlay = document.createElement("div");
|
|
||||||
overlay.classList.add("loading-overlay");
|
|
||||||
overlay.style.position = "absolute";
|
|
||||||
wrapper.appendChild(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = overlay.parentElement;
|
|
||||||
if (wrapper.parentElement !== dom.parentElement)
|
|
||||||
dom.parentNode.appendChild(wrapper);
|
|
||||||
|
|
||||||
const wrapperRect = wrapper.getBoundingClientRect();
|
|
||||||
const domRect = dom.getBoundingClientRect();
|
|
||||||
overlay.style.width = `${dom.scrollWidth}px`;
|
|
||||||
overlay.style.height = `${dom.scrollHeight}px`;
|
|
||||||
overlay.style.left = `${domRect.left - wrapperRect.left}px`;
|
|
||||||
overlay.style.top = `${domRect.top - wrapperRect.top}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
const queries = vnode.attrs["queries"];
|
|
||||||
loading = queries.some((q) => q.fulfilling);
|
|
||||||
return vnode.children;
|
|
||||||
},
|
|
||||||
oncreate: apply,
|
|
||||||
onupdate: apply,
|
|
||||||
onremove: () => {
|
|
||||||
if (overlay) overlay.parentElement.remove();
|
|
||||||
if (dom) dom.classList.remove("loading");
|
|
||||||
overlay = null;
|
|
||||||
dom = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
@@ -38,11 +38,6 @@ const component: ClosureComponent = (): Component => {
|
|||||||
return m(
|
return m(
|
||||||
"tr",
|
"tr",
|
||||||
{
|
{
|
||||||
oncreate: (vn) => {
|
|
||||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
|
||||||
? ""
|
|
||||||
: "none";
|
|
||||||
},
|
|
||||||
onupdate: (vn) => {
|
onupdate: (vn) => {
|
||||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
? ""
|
? ""
|
||||||
@@ -54,11 +49,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return m(
|
return m("table.parameter-list", rows);
|
||||||
"loading",
|
|
||||||
{ queries: [vnode.attrs["deviceQuery"]] },
|
|
||||||
m("table.parameter-list", rows)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,14 +149,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
let label;
|
let label;
|
||||||
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
|
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
|
||||||
|
|
||||||
return [
|
return [label, m("table.table", thead, m("tbody", rows))];
|
||||||
label,
|
|
||||||
m(
|
|
||||||
"loading",
|
|
||||||
{ queries: [vnode.attrs["deviceQuery"]] },
|
|
||||||
m("table.table", thead, m("tbody", rows))
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ const evaluateParam = memoize((exp, obj, now: number) => {
|
|||||||
timestamp = p.valueTimestamp;
|
timestamp = p.valueTimestamp;
|
||||||
value = p.value[0];
|
value = p.value[0];
|
||||||
parameter = exp[1];
|
parameter = exp[1];
|
||||||
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
|
|
||||||
value = new Date(value).toLocaleString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +76,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const { value, timestamp, parameter } = evaluateParam(
|
const { value, timestamp, parameter } = evaluateParam(
|
||||||
vnode.attrs["parameter"],
|
vnode.attrs["parameter"],
|
||||||
vnode.attrs["device"],
|
vnode.attrs["device"],
|
||||||
store.getTimestamp() + store.getClockSkew()
|
store.getTimestamp()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@@ -117,7 +115,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
// Don't update any child element
|
// Don't update any child element
|
||||||
if (e.target === (el as VnodeDOM).dom) {
|
if (e.target === (el as VnodeDOM).dom) {
|
||||||
const now = Date.now() + store.getClockSkew();
|
const now = Date.now();
|
||||||
const localeString = new Date(timestamp).toLocaleString();
|
const localeString = new Date(timestamp).toLocaleString();
|
||||||
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,9 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
||||||
|
|
||||||
let h;
|
let h;
|
||||||
try {
|
if (param && param.value) {
|
||||||
const url = new URL(param.value[0]);
|
const url = new URL(param.value[0]);
|
||||||
h = url.hostname;
|
h = url.hostname;
|
||||||
} catch (err) {
|
|
||||||
// Ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host !== h) {
|
if (host !== h) {
|
||||||
@@ -79,7 +77,7 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m("div", host ? `Pinging ${host}:` : "");
|
return m("div", `Pinging ${host}:`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${deviceId}: Tags updated`
|
`${deviceId}: Tags updated`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
@@ -99,7 +99,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
notifications.push("success", `${deviceId}: Tags updated`);
|
notifications.push("success", `${deviceId}: Tags updated`);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
|
|||||||
+20
-24
@@ -60,12 +60,12 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
.resourceExists("config", id)
|
.resourceExists("config", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Config already exists" });
|
return void resolve({ _id: "Config already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Config does not exist" });
|
return void resolve({ _id: "Config does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
"success",
|
"success",
|
||||||
`Config ${exists ? "updated" : "created"}`
|
`Config ${exists ? "updated" : "created"}`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
})
|
||||||
@@ -87,11 +87,11 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
.deleteResource("config", object["_id"])
|
.deleteResource("config", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Config deleted");
|
notifications.push("success", "Config deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -164,11 +164,11 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
} else {
|
} else {
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
resolve(null);
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -255,7 +255,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -267,7 +267,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
} else {
|
} else {
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
@@ -337,11 +337,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
}
|
}
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -366,18 +366,14 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing config"),
|
m("h1", "Listing config"),
|
||||||
m(
|
m(
|
||||||
"loading",
|
".all-parameters",
|
||||||
{ queries: [confs] },
|
search,
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
".parameter-list",
|
||||||
search,
|
{ style: "height: 400px" },
|
||||||
m(
|
renderTable(confs, vnode.state["searchString"])
|
||||||
".parameter-list",
|
),
|
||||||
{ style: "height: 400px" },
|
m(".actions-bar", [newConfig].concat(subs))
|
||||||
renderTable(confs, vnode.state["searchString"])
|
|
||||||
),
|
|
||||||
m(".actions-bar", [newConfig].concat(subs))
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-62
@@ -46,7 +46,6 @@ button,
|
|||||||
select,
|
select,
|
||||||
option,
|
option,
|
||||||
textarea {
|
textarea {
|
||||||
outline-color: var(--color4);
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
}
|
}
|
||||||
@@ -107,7 +106,7 @@ body {
|
|||||||
|
|
||||||
& > .drawer-wrapper {
|
& > .drawer-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 101;
|
||||||
width: 720px;
|
width: 720px;
|
||||||
margin-left: -360px;
|
margin-left: -360px;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -286,21 +285,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .logo {
|
& > .logo {
|
||||||
display: inline-block;
|
margin: 10px;
|
||||||
position: relative;
|
display: inline;
|
||||||
& > img {
|
height: 56px;
|
||||||
margin: 10px;
|
|
||||||
height: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .version {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 5px;
|
|
||||||
right: 10px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .user-menu {
|
& > .user-menu {
|
||||||
@@ -342,18 +329,12 @@ body {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
|
|
||||||
/* Workaround for Chrome ignoring bottom margin when flex container overflows */
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table {
|
table.table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
& table.table {
|
& table.table {
|
||||||
@@ -759,41 +740,3 @@ span.parameter-value {
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
&::after {
|
|
||||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEwIDUwYTQwIDQwIDAgMDA4MCAwIDQwIDQyIDAgMDEtODAgMCIgZmlsbD0iI2IxMmQ1YyIgLz48L3N2Zz4=);
|
|
||||||
content: "";
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
background-color: #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 3px;
|
|
||||||
& > .progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #b12d5c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2013-2021 GenieACS Inc.
|
|
||||||
*
|
|
||||||
* This file is part of GenieACS.
|
|
||||||
*
|
|
||||||
* GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* GenieACS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import m, { ClosureComponent, Component, Vnode } from "mithril";
|
|
||||||
|
|
||||||
const elements: Map<string, Vnode> = new Map();
|
|
||||||
|
|
||||||
// Source: https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
|
||||||
function hash(str: string): number {
|
|
||||||
let res = 0;
|
|
||||||
for (let i = 0; i < str.length; ++i) {
|
|
||||||
const c = str.charCodeAt(i);
|
|
||||||
res = (res << 5) - res + c;
|
|
||||||
res |= 0;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDatalistId(options: string[]): string {
|
|
||||||
const id = "datalist" + options.reduce((acc, cur) => acc ^ hash(cur), 0);
|
|
||||||
if (!elements.has(id)) {
|
|
||||||
const n = m(
|
|
||||||
"datalist",
|
|
||||||
{ id },
|
|
||||||
options.map((o) => m("option", { value: o }))
|
|
||||||
);
|
|
||||||
elements.set(id, n);
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: () => {
|
|
||||||
return [...elements.values()];
|
|
||||||
},
|
|
||||||
onupdate: () => {
|
|
||||||
for (const id of elements.keys()) {
|
|
||||||
const used = document.querySelector(`[list='${id}']`);
|
|
||||||
if (!used) elements.delete(id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
+4
-16
@@ -42,25 +42,13 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
||||||
|
|
||||||
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
|
||||||
if (!dev.value.length) {
|
if (!dev.length) return "Loading";
|
||||||
if (!dev.fulfilling)
|
|
||||||
return m("p.error", `No such device ${vnode.attrs["deviceId"]}`);
|
|
||||||
return m(
|
|
||||||
"loading",
|
|
||||||
{ queries: [dev] },
|
|
||||||
m("div", { style: "height: 100px;" })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const conf = config.ui.device;
|
const conf = config.ui.device;
|
||||||
const cmps = [];
|
const cmps = [];
|
||||||
|
|
||||||
for (const c of Object.values(conf)) {
|
for (const c of Object.values(conf))
|
||||||
cmps.push(
|
cmps.push(m.context({ device: dev[0] }, c["type"], c));
|
||||||
m.context({ device: dev.value[0], deviceQuery: dev }, c["type"], c)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-10
@@ -154,14 +154,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.deleteResource("devices", id)
|
.deleteResource("devices", id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Deleted`);
|
notifications.push("success", `${id}: Deleted`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Delete"
|
"Delete"
|
||||||
@@ -186,14 +186,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.updateTags(id, { [tag]: true })
|
.updateTags(id, { [tag]: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Tags updated`);
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Tag"
|
"Tag"
|
||||||
@@ -220,14 +220,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.updateTags(id, { [tag]: false })
|
.updateTags(id, { [tag]: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Tags updated`);
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.setTimestamp(Date.now());
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Untag"
|
"Untag"
|
||||||
@@ -336,7 +336,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing devices"),
|
m("h1", "Listing devices"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)),
|
m(indexTableComponent, attrs),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+139
-17
@@ -76,13 +76,13 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const type = task.parameterValues[0][2];
|
|
||||||
let value = task.parameterValues[0][1];
|
|
||||||
if (type === "xsd:dateTime" && typeof value === "number")
|
|
||||||
value = new Date(value).toJSON() || value;
|
|
||||||
input = m("input", {
|
input = m("input", {
|
||||||
type: ["xsd:int", "xsd:unsignedInt"].includes(type) ? "number" : "text",
|
type: ["xsd:dateTime", "xsd:int", "xsd:unsignedInt"].includes(
|
||||||
value: value,
|
task.parameterValues[0][2]
|
||||||
|
)
|
||||||
|
? "number"
|
||||||
|
: "text",
|
||||||
|
value: task.parameterValues[0][1],
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
task.parameterValues[0][1] = input.dom.value;
|
task.parameterValues[0][1] = input.dom.value;
|
||||||
@@ -121,15 +121,12 @@ function renderStagingDownload(task: StageTask): Children {
|
|||||||
if (productClass) productClass = decodeURIComponent(productClass);
|
if (productClass) productClass = decodeURIComponent(productClass);
|
||||||
|
|
||||||
const typesList = [
|
const typesList = [
|
||||||
...new Set([
|
"",
|
||||||
"",
|
"1 Firmware Upgrade Image",
|
||||||
"1 Firmware Upgrade Image",
|
"2 Web Content",
|
||||||
"2 Web Content",
|
"3 Vendor Configuration File",
|
||||||
"3 Vendor Configuration File",
|
"4 Tone File",
|
||||||
"4 Tone File",
|
"5 Ringer File",
|
||||||
"5 Ringer File",
|
|
||||||
...files.value.map((f) => f["metadata.fileType"]).filter((f) => f),
|
|
||||||
]),
|
|
||||||
].map((t) =>
|
].map((t) =>
|
||||||
m(
|
m(
|
||||||
"option",
|
"option",
|
||||||
@@ -187,6 +184,122 @@ function renderStagingDownload(task: StageTask): Children {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadTypesForDevice: WeakMap<
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, string>
|
||||||
|
> = new WeakMap();
|
||||||
|
function getUploadTypesForDevice(
|
||||||
|
device: Record<string, unknown>
|
||||||
|
): Record<string, string> {
|
||||||
|
if (uploadTypesForDevice.has(device)) return uploadTypesForDevice.get(device);
|
||||||
|
const instances: Set<string> = new Set();
|
||||||
|
const filenames: Record<string, string> = {};
|
||||||
|
const versions: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
for (const [k, v] of Object.entries(device)) {
|
||||||
|
if (/DeviceInfo\.VendorConfigFile\.[0-9]+(\.Name|\.Version)?$/.test(k)) {
|
||||||
|
const p = k.split(".");
|
||||||
|
if (p[p.length - 1] === "Name") {
|
||||||
|
const n = `3 Vendor Configuration File ${p[p.length - 2]}`;
|
||||||
|
instances.add(n);
|
||||||
|
if (v["value"]) filenames[n] = v["value"][0];
|
||||||
|
} else if (p[p.length - 1] === "Version") {
|
||||||
|
const n = `3 Vendor Configuration File ${p[p.length - 2]}`;
|
||||||
|
instances.add(n);
|
||||||
|
if (v["value"]) versions[n] = v["value"][0];
|
||||||
|
} else {
|
||||||
|
const n = `3 Vendor Configuration File ${p[p.length - 1]}`;
|
||||||
|
instances.add(n);
|
||||||
|
}
|
||||||
|
} else if (/DeviceInfo\.VendorLogFile\.[0-9]+(\.Name)?$/.test(k)) {
|
||||||
|
const p = k.split(".");
|
||||||
|
if (p[p.length - 1] === "Name") {
|
||||||
|
const n = `4 Vendor Log File ${p[p.length - 2]}`;
|
||||||
|
instances.add(n);
|
||||||
|
if (v["value"]) filenames[n] = v["value"][0];
|
||||||
|
} else {
|
||||||
|
const n = `4 Vendor Log File ${p[p.length - 1]}`;
|
||||||
|
instances.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instances.size) {
|
||||||
|
instances.add("1 Vendor Configuration File");
|
||||||
|
instances.add("2 Vendor Log File");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const i of instances) {
|
||||||
|
let n = filenames[i];
|
||||||
|
const v = versions[i];
|
||||||
|
if (!n) n = i.replace(/\s+/g, "_") + ".txt";
|
||||||
|
if (v) n = `${n}-${v}`;
|
||||||
|
res[i] = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadTypesForDevice.set(device, res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStagingUpload(task: StageTask): Children {
|
||||||
|
if (!task.fileName || !task.fileType) invalid.add(task);
|
||||||
|
else invalid.delete(task);
|
||||||
|
|
||||||
|
const commonTypes: Record<string, number> = {};
|
||||||
|
|
||||||
|
const filenames: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const d of task.devices) {
|
||||||
|
const dev = store.fetch("devices", ["=", ["PARAM", "DeviceID.ID"], d])
|
||||||
|
.value[0];
|
||||||
|
if (dev) {
|
||||||
|
const u = getUploadTypesForDevice(dev);
|
||||||
|
Object.assign(filenames, u);
|
||||||
|
for (const k of Object.keys(u))
|
||||||
|
commonTypes[k] = (commonTypes[k] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const typesList = [""];
|
||||||
|
typesList.push(
|
||||||
|
...Object.entries(commonTypes)
|
||||||
|
.filter(([, c]) => c === task.devices.length)
|
||||||
|
.map(([k]) => k)
|
||||||
|
.sort()
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"Fetch ",
|
||||||
|
m(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
onchange: (e) => {
|
||||||
|
task.fileType = e.target.value;
|
||||||
|
task.fileName = filenames[task.fileType];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typesList.map((t) =>
|
||||||
|
m(
|
||||||
|
"option",
|
||||||
|
{ disabled: !t, value: t, selected: (task.fileType || "") === t },
|
||||||
|
t
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
" as ",
|
||||||
|
m("input", {
|
||||||
|
onchange: (e) => {
|
||||||
|
task.fileName = e.target.value;
|
||||||
|
},
|
||||||
|
value: task.fileName || "",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function renderStaging(staging: Set<StageTask>): Child[] {
|
function renderStaging(staging: Set<StageTask>): Child[] {
|
||||||
const elements: Child[] = [];
|
const elements: Child[] = [];
|
||||||
|
|
||||||
@@ -207,6 +320,7 @@ function renderStaging(staging: Set<StageTask>): Child[] {
|
|||||||
if (s.name === "setParameterValues")
|
if (s.name === "setParameterValues")
|
||||||
elms = renderStagingSpv(s, queueFunc, cancelFunc);
|
elms = renderStagingSpv(s, queueFunc, cancelFunc);
|
||||||
else if (s.name === "download") elms = renderStagingDownload(s);
|
else if (s.name === "download") elms = renderStagingDownload(s);
|
||||||
|
else if (s.name === "upload") elms = renderStagingUpload(s);
|
||||||
|
|
||||||
const queue = m(
|
const queue = m(
|
||||||
"button.primary",
|
"button.primary",
|
||||||
@@ -326,6 +440,14 @@ function renderQueue(queue: Set<QueueTask>): Child[] {
|
|||||||
m(".actions", actions)
|
m(".actions", actions)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else if (t.name === "upload") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
`Fetch file ${t.fileName} (${t.fileType})`,
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
|
details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
|
||||||
}
|
}
|
||||||
@@ -358,7 +480,7 @@ function renderNotifications(notifs): Child[] {
|
|||||||
},
|
},
|
||||||
onbeforeremove: (vnode) => {
|
onbeforeremove: (vnode) => {
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -468,7 +590,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
export let codeMirror: typeof import("codemirror");
|
export let codeMirror;
|
||||||
export let yaml: typeof import("yaml");
|
export let yaml;
|
||||||
|
|
||||||
let note;
|
let note;
|
||||||
|
|
||||||
@@ -43,9 +43,11 @@ export function loadCodeMirror(): Promise<void> {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const promises = [
|
const promises = [
|
||||||
import("codemirror"),
|
import(/* webpackChunkName: "codemirror" */ "codemirror"),
|
||||||
import("codemirror/mode/javascript/javascript"),
|
import(
|
||||||
import("codemirror/mode/yaml/yaml"),
|
/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"
|
||||||
|
),
|
||||||
|
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml"),
|
||||||
];
|
];
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then((modules) => {
|
.then((modules) => {
|
||||||
@@ -63,7 +65,7 @@ export function loadYaml(): Promise<void> {
|
|||||||
if (yaml) return Promise.resolve();
|
if (yaml) return Promise.resolve();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
import("yaml")
|
import(/* webpackChunkName: "yaml" */ "yaml")
|
||||||
.then((module) => {
|
.then((module) => {
|
||||||
yaml = module;
|
yaml = module;
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
+12
-13
@@ -27,7 +27,7 @@ import * as notifications from "./notifications";
|
|||||||
import memoize from "../lib/common/memoize";
|
import memoize from "../lib/common/memoize";
|
||||||
import * as smartQuery from "./smart-query";
|
import * as smartQuery from "./smart-query";
|
||||||
import { map, parse, stringify } from "../lib/common/expression-parser";
|
import { map, parse, stringify } from "../lib/common/expression-parser";
|
||||||
import { stringify as yamlStringify } from "../lib/common/yaml";
|
import { loadYaml, yaml } from "./dynamic-loader";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||||
|
|
||||||
@@ -76,7 +76,13 @@ export function init(
|
|||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||||
return Promise.resolve({ filter, sort });
|
return new Promise((resolve, reject) => {
|
||||||
|
loadYaml()
|
||||||
|
.then(() => {
|
||||||
|
resolve({ filter, sort });
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
export const component: ClosureComponent = (): Component => {
|
||||||
@@ -141,11 +147,8 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return m("a", { href: deviceHref }, fault["device"]);
|
return m("a", { href: deviceHref }, fault["device"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attr.id === "message")
|
|
||||||
return m("long-text", { text: fault["message"] });
|
|
||||||
|
|
||||||
if (attr.id === "detail")
|
if (attr.id === "detail")
|
||||||
return m("long-text", { text: yamlStringify(fault["detail"]) });
|
return m("long-text", { text: yaml.stringify(fault["detail"]) });
|
||||||
|
|
||||||
if (attr.id === "timestamp")
|
if (attr.id === "timestamp")
|
||||||
return new Date(fault["timestamp"]).toLocaleString();
|
return new Date(fault["timestamp"]).toLocaleString();
|
||||||
@@ -187,11 +190,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} faults deleted`
|
`${res.length} faults deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -208,11 +211,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing faults"),
|
m("h1", "Listing faults"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [faults, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+83
-98
@@ -45,6 +45,7 @@ const attributes: {
|
|||||||
{
|
{
|
||||||
id: "metadata.fileType",
|
id: "metadata.fileType",
|
||||||
label: "Type",
|
label: "Type",
|
||||||
|
type: "combo",
|
||||||
options: [
|
options: [
|
||||||
"1 Firmware Upgrade Image",
|
"1 Firmware Upgrade Image",
|
||||||
"2 Web Content",
|
"2 Web Content",
|
||||||
@@ -73,32 +74,58 @@ const unpackSmartQuery = memoize((query) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function upload(
|
interface ValidationErrors {
|
||||||
file: File,
|
[prop: string]: string;
|
||||||
headers: Record<string, string>,
|
}
|
||||||
abortSignal?: AbortSignal,
|
|
||||||
progressListener?: (e: ProgressEvent) => void
|
function putActionHandler(action, _object): Promise<ValidationErrors> {
|
||||||
): Promise<void> {
|
return new Promise((resolve, reject) => {
|
||||||
headers = Object.assign(
|
const object = Object.assign({}, _object);
|
||||||
{
|
if (action === "save") {
|
||||||
"Content-Type": "application/octet-stream",
|
const file = object["file"] ? object["file"][0] : null;
|
||||||
},
|
delete object["file"];
|
||||||
headers
|
|
||||||
);
|
if (!file) return void resolve({ file: "File not selected" });
|
||||||
return store.xhrRequest({
|
|
||||||
method: "PUT",
|
const id = file.name;
|
||||||
headers: headers,
|
|
||||||
url: `api/files/${encodeURIComponent(file.name)}`,
|
store
|
||||||
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
.resourceExists("files", id)
|
||||||
body: file,
|
.then((exists) => {
|
||||||
config: (xhr) => {
|
if (exists) {
|
||||||
if (progressListener)
|
store.fulfill(0, Date.now());
|
||||||
xhr.upload.addEventListener("progress", progressListener);
|
return void resolve({ file: "File already exists" });
|
||||||
if (abortSignal) {
|
}
|
||||||
if (abortSignal.aborted) xhr.abort();
|
const headers = Object.assign(
|
||||||
abortSignal.addEventListener("abort", () => xhr.abort());
|
{
|
||||||
}
|
"Content-Type": "application/octet-stream",
|
||||||
},
|
Accept: "application/octet-stream",
|
||||||
|
},
|
||||||
|
object
|
||||||
|
);
|
||||||
|
|
||||||
|
store
|
||||||
|
.xhrRequest({
|
||||||
|
method: "PUT",
|
||||||
|
headers: headers,
|
||||||
|
url: `api/files/${encodeURIComponent(id)}`,
|
||||||
|
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
||||||
|
body: file,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`File ${exists ? "updated" : "created"}`
|
||||||
|
);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Undefined action"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +212,6 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["sortAttributes"] = sortAttributes;
|
attrs["sortAttributes"] = sortAttributes;
|
||||||
attrs["onSortChange"] = onSortChange;
|
attrs["onSortChange"] = onSortChange;
|
||||||
attrs["downloadUrl"] = downloadUrl;
|
attrs["downloadUrl"] = downloadUrl;
|
||||||
attrs["recordActionsCallback"] = (file) => {
|
|
||||||
return [m("a", { href: "api/blob/files/" + file["_id"] }, "Download")];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("files", 3)) {
|
if (window.authorizer.hasAccess("files", 3)) {
|
||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
@@ -198,77 +222,42 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
title: "Create new file",
|
title: "Create new file",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
const abortController = new AbortController();
|
|
||||||
let progress = -1;
|
|
||||||
const comp = m(
|
const comp = m(
|
||||||
putFormComponent,
|
putFormComponent,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: async (action, _object) => {
|
actionHandler: (action, object) => {
|
||||||
if (action !== "save")
|
return new Promise((resolve) => {
|
||||||
throw new Error("Undefined action");
|
putActionHandler(action, object)
|
||||||
const object = Object.assign({}, _object);
|
.then((errors) => {
|
||||||
const file = object["file"]
|
const errorList = errors
|
||||||
? object["file"][0]
|
? Object.values(errors)
|
||||||
: null;
|
: [];
|
||||||
delete object["file"];
|
if (errorList.length) {
|
||||||
if (!file) {
|
for (const err of errorList)
|
||||||
notifications.push("error", "File not selected");
|
notifications.push("error", err);
|
||||||
return;
|
} else {
|
||||||
}
|
overlay.close(cb);
|
||||||
|
}
|
||||||
if (await store.resourceExists("files", file.name)) {
|
resolve();
|
||||||
store.setTimestamp(Date.now());
|
})
|
||||||
notifications.push("error", "File already exists");
|
.catch((err) => {
|
||||||
return;
|
notifications.push("error", err.message);
|
||||||
}
|
resolve();
|
||||||
|
});
|
||||||
const progressListener = (e: ProgressEvent): void => {
|
});
|
||||||
progress = e.loaded / e.total;
|
|
||||||
m.redraw();
|
|
||||||
};
|
|
||||||
|
|
||||||
progress = 0;
|
|
||||||
try {
|
|
||||||
await upload(
|
|
||||||
file,
|
|
||||||
object,
|
|
||||||
abortController.signal,
|
|
||||||
progressListener
|
|
||||||
);
|
|
||||||
store.setTimestamp(Date.now());
|
|
||||||
notifications.push("success", "File created");
|
|
||||||
overlay.close(cb);
|
|
||||||
} catch (err) {
|
|
||||||
notifications.push("error", err.message);
|
|
||||||
}
|
|
||||||
progress = -1;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
formData
|
formData
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
cb = () => {
|
cb = () => comp;
|
||||||
if (progress < 0) return [null, comp];
|
overlay.open(
|
||||||
return [
|
cb,
|
||||||
m(
|
() =>
|
||||||
"div.progress",
|
!comp.state["current"]["modified"] ||
|
||||||
m("div.progress-bar", {
|
confirm("You have unsaved changes. Close anyway?")
|
||||||
style: `width: ${Math.trunc(progress * 100)}%`,
|
);
|
||||||
})
|
|
||||||
),
|
|
||||||
comp,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
overlay.open(cb, () => {
|
|
||||||
if (
|
|
||||||
comp.state["current"]["modified"] &&
|
|
||||||
!confirm("You have unsaved changes. Close anyway?")
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
abortController.abort();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"New"
|
"New"
|
||||||
@@ -296,11 +285,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} files deleted`
|
`${res.length} files deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -318,11 +307,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing files"),
|
m("h1", "Listing files"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [files, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-8
@@ -23,8 +23,6 @@ import drawerComponent from "./drawer-component";
|
|||||||
import userMenu from "./user-menu";
|
import userMenu from "./user-menu";
|
||||||
import adminMenu from "./admin-menu";
|
import adminMenu from "./admin-menu";
|
||||||
import * as overlay from "./overlay";
|
import * as overlay from "./overlay";
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
import datalist from "./datalist";
|
|
||||||
|
|
||||||
const adminPages = [
|
const adminPages = [
|
||||||
"presets",
|
"presets",
|
||||||
@@ -53,11 +51,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
m("#header", [
|
m("#header", [
|
||||||
m(
|
m("img.logo", { src: "logo.svg" }),
|
||||||
"div.logo",
|
|
||||||
m("img", { src: "logo.svg" }),
|
|
||||||
m("span.version", "v" + VERSION)
|
|
||||||
),
|
|
||||||
m(userMenu),
|
m(userMenu),
|
||||||
m(menu, attrs),
|
m(menu, attrs),
|
||||||
m(drawerComponent),
|
m(drawerComponent),
|
||||||
@@ -70,7 +64,6 @@ const component: ClosureComponent = (): Component => {
|
|||||||
])
|
])
|
||||||
),
|
),
|
||||||
overlay.render(),
|
overlay.render(),
|
||||||
m(datalist),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ export function render(): Children {
|
|||||||
},
|
},
|
||||||
onbeforeremove: (vnode) => {
|
onbeforeremove: (vnode) => {
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|||||||
+11
-14
@@ -47,6 +47,7 @@ const attributes = [
|
|||||||
"devices",
|
"devices",
|
||||||
"faults",
|
"faults",
|
||||||
"files",
|
"files",
|
||||||
|
"uploads",
|
||||||
"permissions",
|
"permissions",
|
||||||
"users",
|
"users",
|
||||||
"presets",
|
"presets",
|
||||||
@@ -117,7 +118,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
|
|||||||
.resourceExists("permissions", id)
|
.resourceExists("permissions", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Permission already exists" });
|
return void resolve({ _id: "Permission already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +126,8 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
|
|||||||
.putResource("permissions", id, object)
|
.putResource("permissions", id, object)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Permission created");
|
notifications.push("success", "Permission created");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
})
|
||||||
@@ -136,11 +137,11 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
|
|||||||
.deleteResource("permissions", object["_id"])
|
.deleteResource("permissions", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Permission deleted");
|
notifications.push("success", "Permission deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -291,7 +292,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object)
|
putActionHandler(action, object)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -351,11 +352,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} permissions deleted`
|
`${res.length} permissions deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -373,11 +374,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing permissions"),
|
m("h1", "Listing permissions"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [permissions, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-21
@@ -116,27 +116,21 @@ function drawChart(chartData): Children {
|
|||||||
|
|
||||||
legend.push(m("span.legend-total", `Total: ${total}`));
|
legend.push(m("span.legend-total", `Total: ${total}`));
|
||||||
|
|
||||||
return m(
|
return m("div", { class: "pie-chart" }, [
|
||||||
"loading",
|
m(
|
||||||
{
|
"svg",
|
||||||
queries: Object.values(chartData.slices).map((s) => s["count"]),
|
{
|
||||||
},
|
// Adding 2 as padding; strokes must not be more than 2
|
||||||
m("div", { class: "pie-chart" }, [
|
viewBox: "-102 -102 204 204",
|
||||||
m(
|
width: "204px",
|
||||||
"svg",
|
height: "204px",
|
||||||
{
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
// Adding 2 as padding; strokes must not be more than 2
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||||
viewBox: "-102 -102 204 204",
|
},
|
||||||
width: "204px",
|
paths.concat(links)
|
||||||
height: "204px",
|
),
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
m(".legend", legend),
|
||||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
]);
|
||||||
},
|
|
||||||
paths.concat(links)
|
|
||||||
),
|
|
||||||
m(".legend", legend),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
|
|||||||
+12
-16
@@ -86,12 +86,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.resourceExists("presets", id)
|
.resourceExists("presets", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Preset already exists" });
|
return void resolve({ _id: "Preset already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Preset does not exist" });
|
return void resolve({ _id: "Preset does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +102,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
"success",
|
"success",
|
||||||
`Preset ${exists ? "updated" : "created"}`
|
`Preset ${exists ? "updated" : "created"}`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
})
|
||||||
@@ -113,12 +113,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.deleteResource("presets", object["_id"])
|
.deleteResource("presets", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Preset deleted");
|
notifications.push("success", "Preset deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("Undefined action"));
|
reject(new Error("Undefined action"));
|
||||||
@@ -294,7 +294,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
{
|
{
|
||||||
base: preset,
|
base: preset,
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, false)
|
putActionHandler(action, object, false)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -355,7 +355,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -413,11 +413,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} presets deleted`
|
`${res.length} presets deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -435,11 +435,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing presets"),
|
m("h1", "Listing presets"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [presets, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-23
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.resourceExists("provisions", id)
|
.resourceExists("provisions", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Provision already exists" });
|
return void resolve({ _id: "Provision already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Provision does not exist" });
|
return void resolve({ _id: "Provision does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +82,10 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
"success",
|
"success",
|
||||||
`Provision ${exists ? "updated" : "created"}`
|
`Provision ${exists ? "updated" : "created"}`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(reject);
|
||||||
if (err["code"] === 400 && err["response"]) {
|
|
||||||
reject(new Error(err["response"]));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
@@ -99,11 +93,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.deleteResource("provisions", object["_id"])
|
.deleteResource("provisions", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Provision deleted");
|
notifications.push("success", "Provision deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +214,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
{
|
{
|
||||||
base: provision,
|
base: provision,
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, false)
|
putActionHandler(action, object, false)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -272,7 +266,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -332,11 +326,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} provisions deleted`
|
`${res.length} provisions deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -354,11 +348,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing provisions"),
|
m("h1", "Listing provisions"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [provisions, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
import { VnodeDOM, ClosureComponent, Component, Children } from "mithril";
|
import { VnodeDOM, ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components";
|
import { m } from "./components";
|
||||||
import codeEditorComponent from "./code-editor-component";
|
import codeEditorComponent from "./code-editor-component";
|
||||||
import { getDatalistId } from "./datalist";
|
|
||||||
|
|
||||||
const singular = {
|
const singular = {
|
||||||
presets: "preset",
|
presets: "preset",
|
||||||
@@ -152,14 +151,9 @@ function createField(current, attr, focus): Children {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let datalist: string = null;
|
|
||||||
if (attr.options) datalist = getDatalistId(attr.options);
|
|
||||||
|
|
||||||
return m("input", {
|
return m("input", {
|
||||||
type: attr.type === "password" ? "password" : "text",
|
type: attr.type === "password" ? "password" : "text",
|
||||||
name: attr.id,
|
name: attr.id,
|
||||||
list: datalist,
|
|
||||||
autocomplete: datalist ? "off" : null,
|
|
||||||
disabled: attr.id === "_id" && !current.isNew,
|
disabled: attr.id === "_id" && !current.isNew,
|
||||||
value: current.object[attr.id],
|
value: current.object[attr.id],
|
||||||
oncreate: focus
|
oncreate: focus
|
||||||
|
|||||||
+176
-175
@@ -34,8 +34,6 @@ const memoizedEvaluate = memoize(evaluate);
|
|||||||
let fulfillTimestamp = 0;
|
let fulfillTimestamp = 0;
|
||||||
let connectionNotification, configNotification, versionNotification;
|
let connectionNotification, configNotification, versionNotification;
|
||||||
|
|
||||||
let clockSkew = 0;
|
|
||||||
|
|
||||||
const queries = {
|
const queries = {
|
||||||
filter: new WeakMap(),
|
filter: new WeakMap(),
|
||||||
bookmark: new WeakMap(),
|
bookmark: new WeakMap(),
|
||||||
@@ -60,11 +58,11 @@ const resources: Resources = {};
|
|||||||
for (const r of [
|
for (const r of [
|
||||||
"devices",
|
"devices",
|
||||||
"faults",
|
"faults",
|
||||||
"files",
|
|
||||||
"presets",
|
"presets",
|
||||||
"provisions",
|
"provisions",
|
||||||
"virtualParameters",
|
"virtualParameters",
|
||||||
"files",
|
"files",
|
||||||
|
"uploads",
|
||||||
"config",
|
"config",
|
||||||
"users",
|
"users",
|
||||||
"permissions",
|
"permissions",
|
||||||
@@ -78,14 +76,14 @@ for (const r of [
|
|||||||
}
|
}
|
||||||
|
|
||||||
class QueryResponse {
|
class QueryResponse {
|
||||||
public get fulfilled(): number {
|
public get fulfilled(): boolean {
|
||||||
queries.accessed.set(this, Date.now());
|
queries.accessed.set(this, Date.now());
|
||||||
return queries.fulfilled.get(this) || 0;
|
return !!queries.fulfilled.get(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get fulfilling(): boolean {
|
public get fulfilling(): boolean {
|
||||||
queries.accessed.set(this, Date.now());
|
queries.accessed.set(this, Date.now());
|
||||||
return !(queries.fulfilled.get(this) >= fulfillTimestamp);
|
return queries.fulfilling.has(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get value(): any {
|
public get value(): any {
|
||||||
@@ -95,13 +93,11 @@ class QueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkConnection(): void {
|
function checkConnection(): void {
|
||||||
const now1 = Date.now();
|
|
||||||
m.request({
|
m.request({
|
||||||
url: "status",
|
url: "status",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
background: true,
|
background: true,
|
||||||
extract: (xhr) => {
|
extract: (xhr) => {
|
||||||
const now2 = Date.now();
|
|
||||||
if (xhr.status !== 200) {
|
if (xhr.status !== 200) {
|
||||||
if (!connectionNotification) {
|
if (!connectionNotification) {
|
||||||
connectionNotification = notifications.push(
|
connectionNotification = notifications.push(
|
||||||
@@ -116,21 +112,6 @@ function checkConnection(): void {
|
|||||||
connectionNotification = null;
|
connectionNotification = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const nowAvg = Math.trunc((now1 + now2) / 2);
|
|
||||||
const skew = Date.parse(xhr.getResponseHeader("Date")) - nowAvg;
|
|
||||||
if (Math.abs(skew - clockSkew) > 5000 && now2 - now1 < 1000) {
|
|
||||||
clockSkew = skew;
|
|
||||||
console.warn(
|
|
||||||
`System and server clocks are out of sync. Adding ${clockSkew}ms offset to any client-side time relative calculations.`
|
|
||||||
);
|
|
||||||
setTimestamp(now2);
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore in case of missing or invalid Date header
|
|
||||||
}
|
|
||||||
|
|
||||||
const configChanged =
|
const configChanged =
|
||||||
xhr.getResponseHeader("x-config-snapshot") !== configSnapshot;
|
xhr.getResponseHeader("x-config-snapshot") !== configSnapshot;
|
||||||
const versionChanged =
|
const versionChanged =
|
||||||
@@ -228,7 +209,7 @@ export async function xhrRequest(
|
|||||||
|
|
||||||
export function unpackExpression(exp: Expression): Expression {
|
export function unpackExpression(exp: Expression): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (!Array.isArray(exp)) return exp;
|
||||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
|
const e = memoizedEvaluate(exp, null, fulfillTimestamp);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +295,7 @@ function compareFunction(sort: {
|
|||||||
function findMatches(resourceType, filter, sort, limit): any[] {
|
function findMatches(resourceType, filter, sort, limit): any[] {
|
||||||
let value = [];
|
let value = [];
|
||||||
for (const obj of resources[resourceType].objects.values())
|
for (const obj of resources[resourceType].objects.values())
|
||||||
if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj);
|
if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj);
|
||||||
|
|
||||||
value = value.sort(compareFunction(sort));
|
value = value.sort(compareFunction(sort));
|
||||||
if (limit) value = value.slice(0, limit);
|
if (limit) value = value.slice(0, limit);
|
||||||
@@ -367,7 +348,18 @@ export function fetch(
|
|||||||
return queryResponse;
|
return queryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fulfill(accessTimestamp: number): void {
|
function _fulfill(
|
||||||
|
accessTimestamp: number,
|
||||||
|
_fulfillTimestamp: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
if (_fulfillTimestamp > fulfillTimestamp) {
|
||||||
|
for (const resource of Object.values(resources))
|
||||||
|
resource.combinedFilter = null;
|
||||||
|
fulfillTimestamp = _fulfillTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
const allPromises = [];
|
const allPromises = [];
|
||||||
|
|
||||||
for (const [resourceType, resource] of Object.entries(resources)) {
|
for (const [resourceType, resource] of Object.entries(resources)) {
|
||||||
@@ -381,32 +373,38 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
|
|
||||||
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
|
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
|
||||||
queries.fulfilling.add(queryResponse);
|
queries.fulfilling.add(queryResponse);
|
||||||
let filter = queries.filter.get(queryResponse);
|
|
||||||
filter = unpackExpression(filter);
|
|
||||||
allPromises.push(
|
allPromises.push(
|
||||||
xhrRequest({
|
new Promise((resolve, reject) => {
|
||||||
method: "HEAD",
|
updated = true;
|
||||||
url:
|
let filter = queries.filter.get(queryResponse);
|
||||||
`api/${resourceType}/?` +
|
filter = unpackExpression(filter);
|
||||||
m.buildQueryString({
|
xhrRequest({
|
||||||
filter: memoizedStringify(filter),
|
method: "HEAD",
|
||||||
}),
|
url:
|
||||||
extract: (xhr) => {
|
`api/${resourceType}/?` +
|
||||||
if (xhr.status === 403) throw new Error("Not authorized");
|
m.buildQueryString({
|
||||||
if (!xhr.status) {
|
filter: memoizedStringify(filter),
|
||||||
throw new Error("Server is unreachable");
|
}),
|
||||||
} else if (xhr.status !== 200) {
|
extract: (xhr) => {
|
||||||
throw new Error(
|
if (xhr.status === 403) throw new Error("Not authorized");
|
||||||
`Unexpected response status code ${xhr.status}`
|
if (!xhr.status) {
|
||||||
);
|
throw new Error("Server is unreachable");
|
||||||
}
|
} else if (xhr.status !== 200) {
|
||||||
return +xhr.getResponseHeader("x-total-count");
|
throw new Error(
|
||||||
},
|
`Unexpected response status code ${xhr.status}`
|
||||||
background: false,
|
);
|
||||||
}).then((c) => {
|
}
|
||||||
queries.value.set(queryResponse, c);
|
return +xhr.getResponseHeader("x-total-count");
|
||||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
},
|
||||||
queries.fulfilling.delete(queryResponse);
|
background: true,
|
||||||
|
})
|
||||||
|
.then((c) => {
|
||||||
|
queries.value.set(queryResponse, c);
|
||||||
|
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||||
|
queries.fulfilling.delete(queryResponse);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -431,39 +429,45 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
const limit = queries.limit.get(queryResponse);
|
const limit = queries.limit.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (limit) {
|
if (limit) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
|
||||||
filter = unpackExpression(filter);
|
|
||||||
allPromises.push(
|
allPromises.push(
|
||||||
xhrRequest({
|
new Promise((resolve, reject) => {
|
||||||
method: "GET",
|
updated = true;
|
||||||
url:
|
let filter = queries.filter.get(queryResponse);
|
||||||
`api/${resourceType}/?` +
|
filter = unpackExpression(filter);
|
||||||
m.buildQueryString({
|
xhrRequest({
|
||||||
filter: memoizedStringify(filter),
|
method: "GET",
|
||||||
limit: 1,
|
url:
|
||||||
skip: limit - 1,
|
`api/${resourceType}/?` +
|
||||||
sort: JSON.stringify(sort),
|
m.buildQueryString({
|
||||||
projection: Object.keys(sort).join(","),
|
filter: memoizedStringify(filter),
|
||||||
}),
|
limit: 1,
|
||||||
background: true,
|
skip: limit - 1,
|
||||||
}).then((res) => {
|
sort: JSON.stringify(sort),
|
||||||
if ((res as any[]).length) {
|
projection: Object.keys(sort).join(","),
|
||||||
// Generate bookmark object
|
}),
|
||||||
const bm = Object.keys(sort).reduce((b, k) => {
|
})
|
||||||
if (res[0][k] != null) {
|
.then((res) => {
|
||||||
if (typeof res[0][k] === "object") {
|
if ((res as any[]).length) {
|
||||||
if (res[0][k].value != null) b[k] = res[0][k].value[0];
|
// Generate bookmark object
|
||||||
} else {
|
const bm = Object.keys(sort).reduce((b, k) => {
|
||||||
b[k] = res[0][k];
|
if (res[0][k] != null) {
|
||||||
}
|
if (typeof res[0][k] === "object") {
|
||||||
}
|
if (res[0][k].value != null)
|
||||||
|
b[k] = res[0][k].value[0];
|
||||||
|
} else {
|
||||||
|
b[k] = res[0][k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return b;
|
return b;
|
||||||
}, {});
|
}, {});
|
||||||
queries.bookmark.set(queryResponse, bm);
|
queries.bookmark.set(queryResponse, bm);
|
||||||
} else {
|
} else {
|
||||||
queries.bookmark.delete(queryResponse);
|
queries.bookmark.delete(queryResponse);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -471,88 +475,31 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(allPromises)
|
return new Promise((resolve, reject) => {
|
||||||
.then(() => {
|
Promise.all(allPromises)
|
||||||
let updated = false;
|
.then(() => {
|
||||||
const allPromises2 = [];
|
const allPromises2 = [];
|
||||||
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
|
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
|
||||||
let combinedFilter = null;
|
let combinedFilter = null;
|
||||||
|
|
||||||
for (const queryResponse of toFetch) {
|
|
||||||
let filter = queries.filter.get(queryResponse);
|
|
||||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
|
||||||
const sort = queries.sort.get(queryResponse);
|
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
|
||||||
combinedFilter = or(combinedFilter, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [union, diff] = unionDiff(
|
|
||||||
resources[resourceType].combinedFilter,
|
|
||||||
combinedFilter
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!diff) {
|
|
||||||
for (const queryResponse of toFetch) {
|
for (const queryResponse of toFetch) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = memoizedEvaluate(
|
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
|
||||||
filter,
|
|
||||||
null,
|
|
||||||
fulfillTimestamp + clockSkew
|
|
||||||
);
|
|
||||||
const limit = queries.limit.get(queryResponse);
|
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
||||||
|
combinedFilter = or(combinedFilter, filter);
|
||||||
queries.value.set(
|
|
||||||
queryResponse,
|
|
||||||
findMatches(resourceType, filter, sort, limit)
|
|
||||||
);
|
|
||||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
|
||||||
queries.fulfilling.delete(queryResponse);
|
|
||||||
updated = true;
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleted = new Set<string>();
|
const [union, diff] = unionDiff(
|
||||||
if (!resources[resourceType].combinedFilter)
|
resources[resourceType].combinedFilter,
|
||||||
deleted = new Set(resources[resourceType].objects.keys());
|
combinedFilter
|
||||||
|
);
|
||||||
const combinedFilterDiff = diff;
|
|
||||||
resources[resourceType].combinedFilter = union;
|
|
||||||
|
|
||||||
allPromises2.push(
|
|
||||||
xhrRequest({
|
|
||||||
method: "GET",
|
|
||||||
url:
|
|
||||||
`api/${resourceType}/?` +
|
|
||||||
m.buildQueryString({
|
|
||||||
filter: memoizedStringify(combinedFilterDiff),
|
|
||||||
}),
|
|
||||||
background: false,
|
|
||||||
}).then((res) => {
|
|
||||||
for (const r of res as any[]) {
|
|
||||||
const id =
|
|
||||||
resourceType === "devices"
|
|
||||||
? r["DeviceID.ID"].value[0]
|
|
||||||
: r["_id"];
|
|
||||||
resources[resourceType].objects.set(id, r);
|
|
||||||
deleted.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const d of deleted) {
|
|
||||||
const obj = resources[resourceType].objects.get(d);
|
|
||||||
if (
|
|
||||||
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
|
|
||||||
)
|
|
||||||
resources[resourceType].objects.delete(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!diff) {
|
||||||
for (const queryResponse of toFetch) {
|
for (const queryResponse of toFetch) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = unpackExpression(filter);
|
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
|
||||||
const limit = queries.limit.get(queryResponse);
|
const limit = queries.limit.get(queryResponse);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
@@ -565,33 +512,87 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||||
queries.fulfilling.delete(queryResponse);
|
queries.fulfilling.delete(queryResponse);
|
||||||
}
|
}
|
||||||
})
|
continue;
|
||||||
);
|
}
|
||||||
}
|
|
||||||
if (updated) m.redraw();
|
updated = true;
|
||||||
return Promise.all(allPromises2);
|
let deleted = new Set<string>();
|
||||||
})
|
if (!resources[resourceType].combinedFilter)
|
||||||
.catch((err) => {
|
deleted = new Set(resources[resourceType].objects.keys());
|
||||||
notifications.push("error", err.message);
|
|
||||||
});
|
const combinedFilterDiff = diff;
|
||||||
|
resources[resourceType].combinedFilter = union;
|
||||||
|
|
||||||
|
allPromises2.push(
|
||||||
|
new Promise((resolve2, reject2) => {
|
||||||
|
xhrRequest({
|
||||||
|
method: "GET",
|
||||||
|
url:
|
||||||
|
`api/${resourceType}/?` +
|
||||||
|
m.buildQueryString({
|
||||||
|
filter: memoizedStringify(combinedFilterDiff),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
for (const r of res as any[]) {
|
||||||
|
const id =
|
||||||
|
resourceType === "devices"
|
||||||
|
? r["DeviceID.ID"].value[0]
|
||||||
|
: r["_id"];
|
||||||
|
resources[resourceType].objects.set(id, r);
|
||||||
|
deleted.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of deleted) {
|
||||||
|
const obj = resources[resourceType].objects.get(d);
|
||||||
|
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
|
||||||
|
resources[resourceType].objects.delete(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const queryResponse of toFetch) {
|
||||||
|
let filter = queries.filter.get(queryResponse);
|
||||||
|
filter = unpackExpression(filter);
|
||||||
|
const limit = queries.limit.get(queryResponse);
|
||||||
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
|
const sort = queries.sort.get(queryResponse);
|
||||||
|
if (bookmark) filter = limitFilter(filter, sort, bookmark);
|
||||||
|
|
||||||
|
queries.value.set(
|
||||||
|
queryResponse,
|
||||||
|
findMatches(resourceType, filter, sort, limit)
|
||||||
|
);
|
||||||
|
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||||
|
queries.fulfilling.delete(queryResponse);
|
||||||
|
}
|
||||||
|
resolve2();
|
||||||
|
})
|
||||||
|
.catch(reject2);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Promise.all(allPromises2)
|
||||||
|
.then(() => resolve(updated))
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fulfill(
|
||||||
|
accessTimestamp: number,
|
||||||
|
_fulfillTimestamp: number,
|
||||||
|
callback?: (updated: boolean) => void
|
||||||
|
): void {
|
||||||
|
_fulfill(accessTimestamp, _fulfillTimestamp).then(callback, (err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
if (callback) callback(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTimestamp(): number {
|
export function getTimestamp(): number {
|
||||||
return fulfillTimestamp;
|
return fulfillTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTimestamp(t: number): void {
|
|
||||||
if (t > fulfillTimestamp) {
|
|
||||||
fulfillTimestamp = t;
|
|
||||||
for (const resource of Object.values(resources))
|
|
||||||
resource.combinedFilter = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getClockSkew(): number {
|
|
||||||
return clockSkew;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function postTasks(
|
export function postTasks(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
tasks: QueueTask[]
|
tasks: QueueTask[]
|
||||||
@@ -693,7 +694,7 @@ export function evaluateExpression(
|
|||||||
obj: Record<string, unknown>
|
obj: Record<string, unknown>
|
||||||
): Expression {
|
): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (!Array.isArray(exp)) return exp;
|
||||||
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
|
return memoizedEvaluate(exp, obj, fulfillTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changePassword(
|
export function changePassword(
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ export function stageDownload(task: StageTask): void {
|
|||||||
staging.add(task);
|
staging.add(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stageUpload(task: StageTask): void {
|
||||||
|
if (queue.size + task.devices.length > MAX_QUEUE) {
|
||||||
|
notifications.push("error", "Too many tasks in queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
staging.add(task);
|
||||||
|
}
|
||||||
|
|
||||||
export function commit(
|
export function commit(
|
||||||
tasks: QueueTask[],
|
tasks: QueueTask[],
|
||||||
callback: (
|
callback: (
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
|||||||
for (const f of res) current[f._id] = f.value;
|
for (const f of res) current[f._id] = f.value;
|
||||||
|
|
||||||
const diff = configFunctions.diffConfig(current, updated);
|
const diff = configFunctions.diffConfig(current, updated);
|
||||||
if (!diff.add.length && !diff.remove.length)
|
if (!diff.add.length && !diff.remove.length) return void resolve();
|
||||||
return void resolve(null);
|
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
|||||||
|
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-18
@@ -85,12 +85,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.resourceExists("users", id)
|
.resourceExists("users", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "User already exists" });
|
return void resolve({ _id: "User already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "User does not exist" });
|
return void resolve({ _id: "User does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +102,14 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.changePassword(id, password)
|
.changePassword(id, password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "User created");
|
notifications.push("success", "User created");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else {
|
} else {
|
||||||
notifications.push("success", "User updated");
|
notifications.push("success", "User updated");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
@@ -120,11 +120,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.deleteResource("users", object["_id"])
|
.deleteResource("users", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "User deleted");
|
notifications.push("success", "User deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -247,7 +247,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
roles: user.roles.split(","),
|
roles: user.roles.split(","),
|
||||||
},
|
},
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, false)
|
putActionHandler(action, object, false)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -329,7 +329,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -387,11 +387,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} users deleted`
|
`${res.length} users deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -409,11 +409,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing users"),
|
m("h1", "Listing users"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [users, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.resourceExists("virtualParameters", id)
|
.resourceExists("virtualParameters", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Virtual parameter already exists" });
|
return void resolve({ _id: "Virtual parameter already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
return void resolve({ _id: "Virtual parameter does not exist" });
|
return void resolve({ _id: "Virtual parameter does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +82,10 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
"success",
|
"success",
|
||||||
`Virtual parameter ${exists ? "updated" : "created"}`
|
`Virtual parameter ${exists ? "updated" : "created"}`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(reject);
|
||||||
if (err["code"] === 400 && err["response"]) {
|
|
||||||
reject(new Error(err["response"]));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
@@ -99,11 +93,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
.deleteResource("virtualParameters", object["_id"])
|
.deleteResource("virtualParameters", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Virtual parameter deleted");
|
notifications.push("success", "Virtual parameter deleted");
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
resolve(null);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +214,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
{
|
{
|
||||||
base: virtualParameter,
|
base: virtualParameter,
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, false)
|
putActionHandler(action, object, false)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -272,7 +266,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -332,11 +326,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} virtual parameters deleted`
|
`${res.length} virtual parameters deleted`
|
||||||
);
|
);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.setTimestamp(Date.now());
|
store.fulfill(0, Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -354,11 +348,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return [
|
return [
|
||||||
m("h1", "Listing virtual parameters"),
|
m("h1", "Listing virtual parameters"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(indexTableComponent, attrs),
|
||||||
"loading",
|
|
||||||
{ queries: [virtualParameters, count] },
|
|
||||||
m(indexTableComponent, attrs)
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user