diff --git a/Makefile b/Makefile index 1285bfad5b837c4b6c7937ad168a55a3d6802e46..07025426d938bd47921f2688a2f30aa6613f0a70 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,14 @@ FOO="bar" PYTHON_VENV_PATH="/opt/c3printing/" CONTAINER_IMAGE_NAME_APP=c3printing-app +CONTAINER_IMAGE_NAME_INFLUXDB=c3printing-influxdb CONTAINER_IMAGE_NAME_CUPS=c3printing-cups CONTAINER_IMAGE_NAME_PROXY=c3printing-proxy CONTAINER_IMAGE_VERSION=latest -CONTAINER_NAME_APP=c3printing-app CONTAINER_NAME_CUPS=c3printing-cups +CONTAINER_NAME_INFLUXDB=c3printing-influxdb +# CONTAINER_NAME_INFLUXDB=influxdb +CONTAINER_NAME_APP=c3printing-app CONTAINER_NAME_PROXY=c3printing-proxy DOCKER_COMPOSE_CONF_PATH=foo @@ -23,13 +26,16 @@ help: docker_build_cups: ## Build local docker image from Dockerfile from cups. docker build . -t ${CONTAINER_IMAGE_NAME_CUPS}:${CONTAINER_IMAGE_VERSION} -f docker/cups/Dockerfile +docker_build_influxdb: ## Build local docker image from Dockerfile from influxdb. + docker build . -t ${CONTAINER_IMAGE_NAME_CUPS}:${CONTAINER_IMAGE_VERSION} -f docker/cups/Dockerfile + docker_build_app: ## Build local docker image from Dockerfile from app. docker build . -t ${CONTAINER_IMAGE_NAME_APP}:${CONTAINER_IMAGE_VERSION} -f docker/app/Dockerfile docker_build_proxy: ## Build local docker image from Dockerfile from proxy. docker build . -t ${CONTAINER_IMAGE_NAME_PROXY}:${CONTAINER_IMAGE_VERSION} -f docker/proxy/Dockerfile -docker_build_all: docker_build_app docker_build_cups docker_build_proxy ## Build local docker image from Dockerfile from all. +docker_build_all: docker_build_cups docker_build_influxdb docker_build_app docker_build_proxy ## Build local docker image from Dockerfile from all. ### ### DOCKER CREATE @@ -37,13 +43,16 @@ docker_build_all: docker_build_app docker_build_cups docker_build_proxy ## Build docker_create_cups: ## Create the local cups container. docker compose -f docker/docker-compose.yml create cups +docker_create_influxdb: ## Create the local inluxdb container. + docker compose -f docker/docker-compose.yml create influxdb + docker_create_app: ## Create the local app container. docker compose -f docker/docker-compose.yml create app docker_create_proxy: ## Create the local app container. docker compose -f docker/docker-compose.yml create proxy -docker_create_all: docker_create_cups docker_create_app docker_create_proxy ## Create all containers. +docker_create_all: docker_create_cups docker_create_influxdb docker_create_app docker_create_proxy ## Create all containers. ### ### DOCKER START @@ -51,16 +60,19 @@ docker_create_all: docker_create_cups docker_create_app docker_create_proxy ## C docker_start_cups: ## Start the local cups container. docker compose -f docker/docker-compose.yml start cups +docker_start_influxdb: ## Start the local influxdb container. + docker compose -f docker/docker-compose.yml start influxdb + docker_start_app: ## Start the local cups container. docker compose -f docker/docker-compose.yml start app docker_start_proxy: ## Start the local proxy container. docker compose -f docker/docker-compose.yml start proxy -docker_start_all: docker_start_cups docker_start_app docker_start_proxy ## Start all containers. +docker_start_all: docker_start_cups docker_start_influxdb docker_start_app docker_start_proxy ## Start all containers. ### -### RUN +### DOCKER RUN ### docker_run: ## Runs the app at the containers. docker compose -f docker/docker-compose.yml up -d @@ -72,13 +84,16 @@ docker_run: ## Runs the app at the containers. docker_stop_app: ## Stop the local app container. docker compose -f docker/docker-compose.yml stop app +docker_stop_influxdb: ## Stop the local influxdb container. + docker compose -f docker/docker-compose.yml stop influxdb + docker_stop_cups: ## Stop the local cups container. docker compose -f docker/docker-compose.yml stop cups docker_stop_proxy: ## Stop the local proxy container. docker compose -f docker/docker-compose.yml stop proxy -docker_stop_all: docker_stop_proxy docker_stop_app docker_stop_cups ## Stop the all containers. +docker_stop_all: docker_stop_proxy docker_stop_app docker_stop_influxdb docker_stop_cups ## Stop the all containers. ### ### DOCKER REMOVE @@ -108,7 +123,7 @@ local_test_lint: ## Runs locally lint tests locally. find ./src -type f -name "*.py" | xargs pylint local_test_unit_single: ## Runs locally unit-tests locally for app_printer. - python3 /home/fejao/.local/lib/python3.10/site-packages/pytest/__main__.py tests/utils/test_app_printer.py + python3 /home/fejao/.local/lib/python3.10/site-packages/pytest/__main__.py tests/web/test_server.py local_test_unit_all: ## Runs locally unit-tests locally for all files. python3 -m pytest @@ -119,12 +134,15 @@ local_run: ## Runs locally the app locally. python3 main.py ### DEBUG -debug_container_connect_app: ## Connects to the app container. - docker exec -it ${CONTAINER_NAME_APP} /bin/bash - debug_container_connect_cups: ## Connects to the cups container. docker exec -it ${CONTAINER_NAME_CUPS} /bin/bash +debug_container_connect_influxdb: ## Connects to the app container. + docker exec -it ${CONTAINER_NAME_INFLUXDB} /bin/bash + +debug_container_connect_app: ## Connects to the app container. + docker exec -it ${CONTAINER_NAME_APP} /bin/bash + debug_container_connect_proxy: ## Connects to the proxy container. docker exec -it ${CONTAINER_NAME_PROXY} /bin/bash @@ -136,3 +154,11 @@ debug_proxy_start: docker_create_proxy docker_start_proxy ## Debug start the pro debug_proxy_remove: ## Debug remove the proxy container. docker stop ${CONTAINER_NAME_PROXY} &&\ docker rm ${CONTAINER_NAME_PROXY} + +debug_influxdb_remove: ## Debug remove the proxy container. + docker stop ${CONTAINER_NAME_INFLUXDB} &&\ + docker rm ${CONTAINER_NAME_INFLUXDB} + +debug_influxdb_set: ## Debug remove the proxy container. + docker compose -f docker/docker-compose.yml create influxdb &&\ + docker compose -f docker/docker-compose.yml start influxdb diff --git a/README.md b/README.md index 58073c8e7dbbddd9b7ab4c78be20f0f7c12ff988..fd6f80358b255bd5cddaa13598af2885d51b4d2e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# c3infodesk - ---- +# c3InfoDesk Printer ## WIP - [x] Set the app for running over containers. @@ -9,6 +7,11 @@ --- +## What is this? +This is a repo for deploying the the printer system used at the c3infodesk for printing EXTREMILY NECESSARY documents + +--- + ## TL;DR ### 1- Clone the repo @@ -31,20 +34,76 @@ make docker_run --- -## Printers list -https://wiki.cccv.de/general/drucker +## Using the system with Docker containers + +### CUPS Problem +There's till the moment, no solution for setting the CUPS container without using the UI. +If you know any other way for doing it, please make a merge request. + +#### Stopping the host CUPS +First of all, at the host system, you should disable cups with: +```bash +systemctl stop cups +``` + +#### Find the Printer on the host +For using it with docker, you have first scan at the host system which port is the printer set +```bash +lsusb +``` + +You should update this at the **docker-compose.yml** file at: +```yaml + cups: + devices: + - /dev/bus/usb/001/007:/dev/bus/usb/001/007 +``` + +So the container can connect to the printer. + +#### Setting the Printer with it's Driver +Now you can go to the running CUPS from the container at the address: + +``` +http://localhosl:631 +``` + +And at the administration set the printer with it's driver or PPD file. + +**NOTE** +HP Printers are tricky to find the right driver. + +#### After printer with driver installed +Now will can start/re-start the others containers from the system + + +### Using InfluxDB +For using the API functionalities, you have to set the **ENABLED_INFLUXDB** to **true** + +Please check also the values at the **docker-compose.yml** and adjust it to your needs. + +#### WIP +At the moment, if the influxDB is enabled, I'm unable to set the App before generating a new token from the InfluxDB-UI :( + +The token used for generating the container can't be used for setting the organization, buckets and etc. +This step have to also be genareted manually, as the same as setting the printer from the CUPS container. +If you have an idea how to solve that, please make a merge request. + +So you have to start the system from the docker-compose and go trought the InfluxDB-UI for generating a new Token, then, update the value from **INFLUXDB_TOKEN** at the **docker-compose.yml** file with the new generated token. +After that, it should work fine. +Restart the containers from **docker-compose.yml** and the app container should be running. --- -## What is this? -This is a repo for deploying the the printer system used at the c3infodesk for printing EXTREMILY NECESSARY documents +## Printers list +https://wiki.cccv.de/general/drucker + --- ## Installing the the system Just clone this repo to your computer/Pi. Use the functions/commands setted over the **Makefile** - --- ## Using the Makefile @@ -91,68 +150,19 @@ local_test_unit Runs locally unit-tests locally. --- ## Adjusting the system -You can set all variables at the **config.yml** file or setting System environmemts variables or parsing the parameters - -### Parsing the values - -```bash -python3 main.py --help - -usage: main.py [-h] [-d] [-v] [-lf] [-umd] [-eq] [-ea] [-pu PATH_TO_UPLOAD] [-pl PATH_TO_LOG] [-pd PATH_TO_DB] - [-pt PATH_TEST_FILE] [-p PORT_SERVER] [-lfm LOG_FILE_MODE] [-f LOG_FORMAT] [-pn PRINTER_NAME] [-lp LOGIN_PASSWD] - [-le LOGIN_EMAIL] [-ka KEEP_ALIVE] [-nd NUM_DIGITS] [-muf MAX_UPLOAD_BYTES_FOLDER] [-mu MAX_UPLOAD_BYTES_FILE] - [-df DATE_FORMAT] [-afe ALLOWED_FILE_EXTENSIONS] [-fpw FILE_PREVIEW_WIDTH] [-fph FILE_PREVIEW_HEIGHT] +You can adjust the system in 3 ways +- Editing the **config.yml** +- Setting **environmental variables** +- Parsing values to the python command -c3 InfoDesk Printer - -options: - -h, --help show this help message and exit - -d, --debug Enable debugging information from execution. - -v, --verbose Enable printing information from execution. - -lf, --log_to_file Enable writing the log to a file. - -umd, --use_mem_db If using the TinyDB in memory or file. - -eq, --enabled_questions - If using the questions functionality. - -ea, --enabled_about If using the about functionality. - -pu PATH_TO_UPLOAD, --path_to_upload PATH_TO_UPLOAD - Path where the printing files should be uploaded. - -pl PATH_TO_LOG, --path_to_log PATH_TO_LOG - Path where the log file should be written. - -pd PATH_TO_DB, --path_to_db PATH_TO_DB - Path where the db file should be written. - -pt PATH_TEST_FILE, --path_test_file PATH_TEST_FILE - Path where the test print file location. - -p PORT_SERVER, --port_server PORT_SERVER - The port of the system - -lfm LOG_FILE_MODE, --log_file_mode LOG_FILE_MODE - Mode that the log file should be written, it can be: 'append' or 'new'. - -f LOG_FORMAT, --log_format LOG_FORMAT - Log format to be used. - -pn PRINTER_NAME, --printer_name PRINTER_NAME - Name of the printer to be used. - -lp LOGIN_PASSWD, --login_passwd LOGIN_PASSWD - Password to be used for login and printing. - -le LOGIN_EMAIL, --login_email LOGIN_EMAIL - Email to be used for login and printing. - -ka KEEP_ALIVE, --keep_alive KEEP_ALIVE - The amount of minutes to keep the file at the server before deletion. - -nd NUM_DIGITS, --num_digits NUM_DIGITS - The lenght of the generated code from the file to be printed. - -muf MAX_UPLOAD_BYTES_FOLDER, --max_upload_bytes_folder MAX_UPLOAD_BYTES_FOLDER - The maximum size in bytes that can be upload folder can keep. - -mu MAX_UPLOAD_BYTES_FILE, --max_upload_bytes_file MAX_UPLOAD_BYTES_FILE - The maximum size in bytes from a file that can be uploaded. - -df DATE_FORMAT, --date_format DATE_FORMAT - The date format to be used handiling the system. - -afe ALLOWED_FILE_EXTENSIONS, --allowed_file_extensions ALLOWED_FILE_EXTENSIONS - The allowed file extensions that the system can use. - -fpw FILE_PREVIEW_WIDTH, --file_preview_width FILE_PREVIEW_WIDTH - The width at the webpage for previewing a file. - -fph FILE_PREVIEW_HEIGHT, --file_preview_height FILE_PREVIEW_HEIGHT - The height at the webpage for previewing a file. +The hierarchy from each value will be over-writen is: +``` +config.yml --> environmental variables --> parsed values ``` +This means that the parsed values will have priority from the other 2 ones. + ### System Environments This are the system environments that can be set for running the script. @@ -160,6 +170,7 @@ This are the system environments that can be set for running the script. - **SERVER_PORT**=8000 - **SERVER_SECRET_KEY**=123456 - **DEBUG**=true +- **DEBUG_DISABLE_PRINTING**=false - **VERBOSE**=true - **LOG_TO_FILE**=false - **PATH_UPLOAD**=/c3printing/uploads @@ -170,6 +181,8 @@ This are the system environments that can be set for running the script. - **LOGIN_USER_PASSWD**=123456 - **LOGIN_ADMIN_EMAIL**=foo@bar.net - **LOGIN_ADMIN_PASSWD**=123456 +- **LOGIN_API_EMAIL**=foo@bar.net +- **LOGIN_API_PASSWD**=123456 - **LOG_MODE**=append - **LOG_FORMAT**=%(asctime)s - %(levelname)s - %(module)s - %(funcName)s - %(message)s - **KEEP_ALIVE_MIN**=15 @@ -179,41 +192,137 @@ This are the system environments that can be set for running the script. - **DATE_FORMAT**=%Y-%m-%d_-_%H-%M-%S - **ALLOWED_FILE_EXTENSIONS**="{'png', 'jpg', 'jpeg', 'gif', 'pdf'}" - **USE_MEM_DB**=false +- **ENABLED_PRINT_WITH_OPTIONS**=true - **ENABLED_QUESTIONS**=false - **ENABLED_ABOUT**=false -- **ENABLED_PRINT_WITH_OPTIONS**=true +- **ENABLED_API**=false +- **ENABLED_INFLUXDB**=false - **FILE_PREVIEW_WIDTH**=800 - **FILE_PREVIEW_HEIGHT**=600 - **FILE_AUTO_DELETE_AFTER_PRINT**=false +- **API_TOKEN_EXPIRE_HOURS**=120 +- **INFLUXDB_TOKEN**='Please set the influxdb token' +- **INFLUXDB_PORT**=8086 +- **INFLUXDB_URL**='http://localhost' +- **INFLUXDB_MEASUREMENT**='app-usage' +- **INFLUXDB_ROTATION_MINUTES**=2 +- **INFLUXDB_BUCKET**='c3infodesk-print' +- **INFLUXDB_BUCKET_DESCRIPTION**='Bucket used from InfoDesk at the 39c3' +- **INFLUXDB_BUCKET_RETENTION_DAYS**=10 ---- - -## Docker container -**WIP** - Working on reconizing the printer on the container using cups -> missing drivers -> ??? update to newer docker img ??? +### Parsing the values +You can see the options by running the command: -First of all, at the host system, you should disable cups with: ```bash -systemctl stop cups +python3 src/main.py --help ``` -For using it with docker, you have first scan at the host system which port is the printer set +The options are: ```bash -lsusb -``` - -You should update this at the docker-compose.yml file at: -```yaml - devices: - - /dev/bus/usb/001/007:/dev/bus/usb/001/007 -``` - -So the container can connect to the printer. - -After, you have to set the printer at the container CUPS +usage: main.py [-h] [--debug] [--debug_disable_printing] [--verbose] [--log_to_file] [--use_mem_db] [--enabled_questions] [--enabled_about] + [--enabled_print_with_options] [--enabled_api] [--enabled_influxdb] [--file_auto_delete_after_print] [--path_to_upload PATH_TO_UPLOAD] + [--path_to_log PATH_TO_LOG] [--path_to_data PATH_TO_DATA] [--path_test_file PATH_TEST_FILE] [--port_server PORT_SERVER] + [--server_secret_key SERVER_SECRET_KEY] [--log_file_mode LOG_FILE_MODE] [--log_format LOG_FORMAT] [--printer_name PRINTER_NAME] + [--login_user_email LOGIN_USER_EMAIL] [--login_user_passwd LOGIN_USER_PASSWD] [--login_admin_email LOGIN_ADMIN_EMAIL] + [--login_admin_passwd LOGIN_ADMIN_PASSWD] [--login_api_email LOGIN_API_EMAIL] [--login_api_passwd LOGIN_API_PASSWD] [--keep_alive KEEP_ALIVE] + [--num_digits NUM_DIGITS] [--max_upload_bytes_folder MAX_UPLOAD_BYTES_FOLDER] [--max_upload_bytes_file MAX_UPLOAD_BYTES_FILE] + [--max_upload_pdf_pages MAX_UPLOAD_PDF_PAGES] [--max_upload_file_copies MAX_UPLOAD_FILE_COPIES] [--date_format DATE_FORMAT] + [--allowed_file_extensions ALLOWED_FILE_EXTENSIONS] [--file_preview_width FILE_PREVIEW_WIDTH] [--file_preview_height FILE_PREVIEW_HEIGHT] + [--api_token_expire_hours API_TOKEN_EXPIRE_HOURS] [--influxdb_token INFLUXDB_TOKEN] [--influxdb_organization INFLUXDB_ORGANIZATION] + [--influxdb_port INFLUXDB_PORT] [--influxdb_url INFLUXDB_URL] [--influxdb_measurement INFLUXDB_MEASUREMENT] + [--influxdb_rotation_minutes INFLUXDB_ROTATION_MINUTES] [--influxdb_bucket INFLUXDB_BUCKET] + [--influxdb_bucket_description INFLUXDB_BUCKET_DESCRIPTION] [--influxdb_bucket_retention_days INFLUXDB_BUCKET_RETENTION_DAYS] -http://localhosl:631 +c3 InfoDesk Printer +options: + -h, --help show this help message and exit + --debug Enable debugging information from execution. + --debug_disable_printing + Disable the printing funcionallity for debugging. + --verbose Enable printing information from execution. + --log_to_file Enable writing the log to a file. + --use_mem_db If using the TinyDB in memory or file. + --enabled_questions If using the questions functionality. + --enabled_about If using the about functionality. + --enabled_print_with_options + If using the functionality for printing with options. + --enabled_api If using the API functionality. + --enabled_influxdb If using the InfluxDB functionality. + --file_auto_delete_after_print + If auto deleting the file after printing. + --path_to_upload PATH_TO_UPLOAD + Path where the printing files should be uploaded. + --path_to_log PATH_TO_LOG + Path where the log file should be written. + --path_to_data PATH_TO_DATA + Path where the app data should be written. + --path_test_file PATH_TEST_FILE + Path where the test print file location. + --port_server PORT_SERVER + The port of the system + --server_secret_key SERVER_SECRET_KEY + The secret key from the server + --log_file_mode LOG_FILE_MODE + Mode that the log file should be written, it can be: 'append' or 'new'. + --log_format LOG_FORMAT + Log format to be used. + --printer_name PRINTER_NAME + Name of the printer to be used. + --login_user_email LOGIN_USER_EMAIL + Email to be used for login as user for printing. + --login_user_passwd LOGIN_USER_PASSWD + Password to be used for login as user for printing. + --login_admin_email LOGIN_ADMIN_EMAIL + Email to be used for login as admin. + --login_admin_passwd LOGIN_ADMIN_PASSWD + Password to be used for login as admin. + --login_api_email LOGIN_API_EMAIL + Email to be used for login as api. + --login_api_passwd LOGIN_API_PASSWD + Password to be used for login as api. + --keep_alive KEEP_ALIVE + The amount of minutes to keep the file at the server before deletion. + --num_digits NUM_DIGITS + The lenght of the generated code from the file to be printed. + --max_upload_bytes_folder MAX_UPLOAD_BYTES_FOLDER + The maximum size in bytes that can be upload folder can keep. + --max_upload_bytes_file MAX_UPLOAD_BYTES_FILE + The maximum size in bytes from a file that can be uploaded. + --max_upload_pdf_pages MAX_UPLOAD_PDF_PAGES + The maximum number of pages from a PDF file that can be uploaded. + --max_upload_file_copies MAX_UPLOAD_FILE_COPIES + The maximum number of copies from a file that can be printed. + --date_format DATE_FORMAT + The date format to be used handiling the system. + --allowed_file_extensions ALLOWED_FILE_EXTENSIONS + The allowed file extensions that the system can use. + --file_preview_width FILE_PREVIEW_WIDTH + The width at the webpage for previewing a file. + --file_preview_height FILE_PREVIEW_HEIGHT + The height at the webpage for previewing a file. + --api_token_expire_hours API_TOKEN_EXPIRE_HOURS + How many hours till the API token to expire. + --influxdb_token INFLUXDB_TOKEN + The token to be used for InfluxDB. + --influxdb_organization INFLUXDB_ORGANIZATION + The organization to be used for InfluxDB. + --influxdb_port INFLUXDB_PORT + The port of the InfluxDB server. + --influxdb_url INFLUXDB_URL + The URL of the InfluxDB server. + --influxdb_measurement INFLUXDB_MEASUREMENT + The measurement name of the InfluxDB server. + --influxdb_rotation_minutes INFLUXDB_ROTATION_MINUTES + how many minutes to refresh the values at the InfluxDB server. + --influxdb_bucket INFLUXDB_BUCKET + The bucket name of the InfluxDB server. + --influxdb_bucket_description INFLUXDB_BUCKET_DESCRIPTION + The description from the bucket of the InfluxDB server. + --influxdb_bucket_retention_days INFLUXDB_BUCKET_RETENTION_DAYS + The retention days from the bucket of the InfluxDB server. +``` --- @@ -243,6 +352,7 @@ http://localhost - To logout at the app as administrator. ### Admin +These end-points are only accessible **when logged into the system**. - **/logout**: GET - Logout as admin. @@ -252,37 +362,82 @@ http://localhost - Show all the uploaded files. Here you can print or delete the uploaded files. - **/uploaded-clean**: GET - Nuke all uploaded files -- **/file-print**: +- **/file-print**: POST - Return page with the message from printing a file. -- **/file-delete**: POST - - Return page with the message from deleting a file. +- **/file-print-options**: POST + - Return page with the message from printing a file with printing options. - **/file-preview**: POST - Opens a new Tab with the file content. -- **/remove-all-files**: POST - - A route for nuking all files on the system. +- **/file-delete**: POST + - Return page with the message from deleting a file. - **/remove-all-files**: POST - A route for nuking all files on the system. - **/docs/index.html**: GET - A route for reading the documentaion of this project. ### Debug +These endpoints are only enable when **DEBUG** is set to **true** -- **/debug-printers**: GET - - Here you can list all the printers recognized from the app using CUPS. -- **/debug-print-test**: GET | POST - - Here you can try to print a test page using the system. So I don't have to upload allways to test printing. +- **/debug-printers**: GET | POST + - Here you can list all the printers recognized from the app using CUPS and printing a test file. +- **/debug-sys**: GET + - Display a couple of tips for debugging the system and also all the setted values. +- **/debug-api**: GET + - Display the generated **token_access** and **token_refresh** values. ### Questions +These endpoints are only enable when **ENABLED_QUESTIONS** is set to **true** -- **/most-asked-questions**: GET | POST +- **/most-asked-questions**: GET | POST - Read, update and add a new most common questions. - **/set-question**: GET | POST - Add or update the questions on the system. --- +--- + +## Accessing the System via API +For using the API functionalities, you have to set the **ENABLED_API** to **true** + + +### POST /api/login +Creating new tokens are only accessible when the app are running at **debug mode** + +```bash +curl -X POST \ + http://localhost:8000/api/login \ + -u api@39c3.local:123456 +``` + +### GET /api/create-refresh-token +```bash +curl -X POST \ + http://localhost:8000/api/create-refresh-token \ + -H 'Authorization: Bearer YOUR_REFRESH_TOKEN' \ + -H 'Content-Type: application/json' \ + -d '{"refresh_token": "YOUR_REFRESH_TOKEN"}' +``` + +### GET /api/test +```bash +curl -X GET \ + http://your-app-url.local/api/test \ + -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \ + -H 'Content-Type: application/json' +``` + +--- + ## InfoDesk Printer ### 37c3 https://support.hp.com/de-de/drivers/hp-laserjet-pro-m402dn/model/7458632 +### 38c3 +TODO: Find this in WIKI + +--- + +## License + diff --git a/ci-cd/requirements.txt b/ci-cd/requirements.txt index 21d3072802caae56b8d6643f5f6b7d9af67bd457..aac4d5e242fb5d561dca2173732eb7e1901fd027 100644 --- a/ci-cd/requirements.txt +++ b/ci-cd/requirements.txt @@ -10,6 +10,10 @@ tinydb APScheduler pytz pypdf +### API +flask_jwt_extended +### INFLUXDB +influxdb-client ### TESTS pylint pylint_junit diff --git a/data/db.json b/data/db.json index 9b2f7955bc0d8d7cadaa8cc871884c7825563c9d..e9b181766639413e5667fca0ef12bf9dc2bcb639 100644 --- a/data/db.json +++ b/data/db.json @@ -1 +1 @@ -{"_default": {"1": {"file_code": "429-359-943", "uploaded_date": "2025-01-15_-_20-01-36", "original_name": "test_file_1.pdf", "new_file_name": "429-359-943___-___2025-01-15_-_20-01-36___-___test_file_1.pdf", "path": "/home/fejao/Coding/CCC/Repos/c3infodesk/c3printer/uploads/429-359-943___-___2025-01-15_-_20-01-36___-___test_file_1.pdf", "num_pages": 1}}} \ No newline at end of file +{"_default": {}} \ No newline at end of file diff --git a/data/usage.ini b/data/usage.ini index 7e8c7cd2121f786db2695ef2abf1730fda693e8a..b700753f697de416019e4bbedca6bd56c7f625fd 100644 --- a/data/usage.ini +++ b/data/usage.ini @@ -2,11 +2,17 @@ total = 1 [PRINTS] -total = 1 +total = 0 [PAGES] -total = 1 +total = 0 [MAX_SIMULTANEOUS_UPLOADS] total = 1 +[FILES] +total = 0 + +[TOTAL_SIZE_UPLOAD] +total = 0 + diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index d903d44a816e49f7c7f61889bc36f42da6818ce8..4c59b58f3b916818dcc9178163758ca9548013e9 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -14,12 +14,17 @@ RUN apt-get update -qq && apt-get upgrade -qqy \ # Copy app to the container WORKDIR /c3printing +COPY ../../src /c3printing/src +RUN pip3 install -r src/requirements.txt + +# Copy the rest of the app to the container COPY ../../data /c3printing/data COPY ../../logs /c3printing/logs COPY ../../uploads /c3printing/uploads -COPY ../../src /c3printing/src -RUN pip3 install -r src/requirements.txt WORKDIR /c3printing/src +### DEBUG +# ENTRYPOINT ["/bin/sh", "-c" , "sleep 10000000"] + ENTRYPOINT ["/bin/sh", "-c" , "python3 main.py"] diff --git a/docker/cups/printers.conf b/docker/cups/printers.conf new file mode 100644 index 0000000000000000000000000000000000000000..23ce6dc1e8157ebf8f9bb7c1289f11485c12f6f0 --- /dev/null +++ b/docker/cups/printers.conf @@ -0,0 +1,24 @@ +# Printer configuration file for CUPS v2.4.2 +# Written by cupsd +# DO NOT EDIT THIS FILE WHEN CUPSD IS RUNNING +NextPrinterId 3 +<Printer HP_LaserJet_M402n> +PrinterId 2 +UUID urn:uuid:d1d9cd1c-e878-3e65-7157-883ead742ade +Info HP LaserJet M402n +Location infodesk +MakeModel HP LaserJet Pro M402-M403 Postscript (recommended) +DeviceURI usb://HP/LaserJet%20M402n?serial=PHCGC58854 +State Idle +StateTime 1735157141 +ConfigTime 1735157159 +Type 8425684 +Accepting Yes +Shared No +JobSheets none none +QuotaPeriod 0 +PageLimit 0 +KLimit 0 +OpPolicy default +ErrorPolicy retry-job +</Printer> diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fa2cede618407bb7f61f6c4bc3873c8a2556f7e5..50e436f1af18af83202231742a2da7e58bc0a023 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,15 +13,52 @@ services: interval: 5s timeout: 5s retries: 5 - # ports: - # - 631:631 + ### THIS SHOULD NOT SET FOR PRODUCTION + ports: + - 631:631 devices: ### You should scan which printer with lsub - - /dev/bus/usb/001/005:/dev/bus/usb/001/005 + # - /dev/bus/usb/001/005:/dev/bus/usb/001/005 + - /dev/bus/usb/001:/dev/bus/usb/001 volumes: - /var/run/dbus:/var/run/dbus - /dev/bus/usb:/dev/bus/usb - cups_socket:/run/cups + ### MOUNT LOCAL -> CONFIG AND PRINTERS + - ./cups/cupsd.conf:/etc/cups/cupsd.conf + - ./cups/printers.conf:/etc/cups/printers.conf + ### + ### INFLUXDB + ### + influxdb: + image: influxdb:2.7.11-alpine + container_name: c3printing-influxdb + # restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8086 || exit 1"] + interval: 5s + timeout: 5s + retries: 5 + ports: + - 8086:8086 + volumes: + - ./influxdb/data:/var/lib/influxdb2 + - ./influxdb/config:/etc/influxdb2 + environment: + - FOO=BAR + # - INFLUXDB_ADMIN_ENABLED=true + - INFLUXDB_TOKEN="myL9Rou80UKyqgHrRXNIOe1YtoU5L15PlIyPwFs-NZoJYLa2X2Mdu0sYgd1FYEEAeaLNW0VeaJYTivE5iBFyiQ==" + ### + ### INIT + ### + - DOCKER_INFLUXDB_INIT_MODE=setup + - DOCKER_INFLUXDB_INIT_USERNAME=admin + - DOCKER_INFLUXDB_INIT_PASSWORD=12345678 + - DOCKER_INFLUXDB_INIT_ORG=c3infodesk + - DOCKER_INFLUXDB_INIT_BUCKET=c3infodesk-print + - DOCKER_INFLUXDB_INIT_RETENTION=1w + # - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN="my-super-secret-auth-token" + - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN="myL9Rou80UKyqgHrRXNIOe1YtoU5L15PlIyPwFs-NZoJYLa2X2Mdu0sYgd1FYEEAeaLNW0VeaJYTivE5iBFyiQ==" ### ### APP ### @@ -38,6 +75,8 @@ services: depends_on: cups: condition: service_healthy + influxdb: + condition: service_healthy links: - cups ports: @@ -48,6 +87,8 @@ services: - ../data:/c3printing/data - ../logs:/c3printing/logs - ../uploads:/c3printing/uploads + ### DEBUG + # - ../src/:/c3printing/src ### ### PLEASE SET THIS FIELDS, most are set from the Docker image ### @@ -60,6 +101,7 @@ services: - PATH_DATA=/c3printing/data - PATH_TEST_FILE=/c3printing/tests/files/test_file.pdf - DEBUG=true + - DEBUG_DISABLE_PRINTING=false - VERBOSE=true - SERVER_PORT=8000 - SERVER_SECRET_KEY=123456 @@ -67,6 +109,8 @@ services: - LOGIN_USER_PASSWD=123456 - LOGIN_ADMIN_EMAIL=admin@39c3.local - LOGIN_ADMIN_PASSWD=123456 + - LOGIN_API_EMAIL=api@39c3.local + - LOGIN_API_PASSWD=123456 - LOG_MODE=append - LOG_TO_FILE=false - KEEP_ALIVE_MIN=15 @@ -79,13 +123,28 @@ services: ### 16 Mb = 16 * 1024 * 1024 = 16777216 - MAX_UPLOAD_BYTES_FILE=16777216 ### If not using the printing with options functionality: true | false - - ENABLED_PRINT_WITH_OPTIONS=true + - ENABLED_PRINT_WITH_OPTIONS=false ### If not using the questions functionality: true | false - ENABLED_QUESTIONS=true + ### If not using the API functionality: true | false + - ENABLED_API=false + ### If not using the InfluxDB functionality: true | false + - ENABLED_INFLUXDB=false ### If using the DB on RAM only: true | false - USE_MEM_DB=false ### If it should delete the files after printing - FILE_AUTO_DELETE_AFTER_PRINT=false + - FILE_PREVIEW_WIDTH=800 + - FILE_PREVIEW_HEIGHT=600 + ### 5 days in hours = 120 + - API_TOKEN_EXPIRE_HOURS=120 + - INFLUXDB_TOKEN='Please set the influxdb token' + - INFLUXDB_PORT=8086 + - INFLUXDB_URL='http://localhost' + - INFLUXDB_BUCKET='c3infodesk-print' + - INFLUXDB_BUCKET_DESCRIPTION='Bucket used from InfoDesk at the 39c3' + - INFLUXDB_BUCKET_RETENTION_DAYS=10 + - INFLUXDB_MEASUREMENT='app-usage' ### ### PROXY ### diff --git a/src/config.yml b/src/config.yml index b8a2109d662182f0a67d49ba4d2cfc1df45a58a9..18c4ba83f644b1ece5053394b44f02b53a35c23b 100644 --- a/src/config.yml +++ b/src/config.yml @@ -15,12 +15,12 @@ default: ### ### SET OWN PROPERTIES: END ### - # login_email: 'foo@bar.net' - # login_passwd: '123456' login_user_email: 'angel@39c3.local' login_user_passwd: '123456' login_admin_email: 'admin@39c3.local' login_admin_passwd: '123456' + login_api_email: 'api@39c3.local' + login_api_passwd: '123456' ### ### ### @@ -36,14 +36,18 @@ default: # debug: false # verbose: false ### + debug_disable_printing: true + # debug_disable_printing: false + ### # log_to_file: true log_to_file: false ### - # log_file_mode: "append" - log_file_mode: "new" + log_file_mode: "append" + # log_file_mode: "new" ### # keep_file_minutes: 15 keep_file_minutes: 1 + # keep_file_minutes: 3 ### # You should use multiples of 3 -> (6 | 9) # num_digits: 6 @@ -66,11 +70,39 @@ default: # enabled_about: true enabled_about: false ### PRINT WITH OPTIONS - # enabled_print_with_options: true - enabled_print_with_options: false + enabled_print_with_options: true + # enabled_print_with_options: false + ### API + enabled_api: true + # enabled_api: false + ### INFLUXDB + enabled_influxdb: true + # enabled_influxdb: false ### FILE PREVIEW file_preview_width: '800' file_preview_height: '900' ### FILE AUTO DELETE AFTER PRINT # file_auto_delete_after_print: true file_auto_delete_after_print: false + ### API TOKEN EXPIRE + # 5 days in hours = 120 + api_token_expire_hours: 120 + ### + ### INFLUXDB + ### + + ### ORIG + # influxdb_token: "my-super-secret-auth-token" + # influxdb_token: "hMDdMGpjJH6AiAZaLpi3A0QXpJXLBiBYewcpmBWc2w-WHUW_fC1L_ZW4Y43Zx1uWZnlJ36SRZH7EqWqfAfa7GA==" + ### SET ON DOCKERFILE + # influxdb_token: "myL9Rou80UKyqgHrRXNIOe1YtoU5L15PlIyPwFs-NZoJYLa2X2Mdu0sYgd1FYEEAeaLNW0VeaJYTivE5iBFyiQ==" + ### + influxdb_token: "TwRkAaLUQIk97IJX9wgWgn4FRN_dm1Tvileh_63-sVdWyB6Xh25taaF_05RD8qiWPsb-BUAZKBv_p2Iq_Qe2Aw==" + influxdb_port: '8086' + influxdb_url: 'http://localhost' + influxdb_organization: 'c3infodesk' + influxdb_bucket: 'c3infodesk-print' + influxdb_measurement: 'app-usage' + influxdb_bucket_description: 'Bucket used from InfoDesk at the 39c3' + influxdb_bucket_retention_days: 10 + influxdb_rotation_minutes: 2 diff --git a/src/main.py b/src/main.py index 63e4cd3687d07d5b19d89d36c2650de17629eec3..dd1c6bb18715dc08e00bba60b5e38944e9a6c0bc 100644 --- a/src/main.py +++ b/src/main.py @@ -49,12 +49,27 @@ def files_cleaner() -> None: """ logger("Starting files_cleaner...") - files_expired = settings._db.get_expired() + files_expired = settings._local_db.get_expired() for file in files_expired: ### DELETE FROM DISK settings._files.file_delete(file) ### DELETE FROM DB - settings._db.file_delete(file) + settings._local_db.file_delete(file) + ### DELETE FROM USAGE + settings._usage.update_from_delete() + + +def rotate_influxdb_values() -> None: + """ + Function to rotate the values at the InfluxDB. + + Returns + ------- + None + """ + + logger("Starting rotate_influxdb_values...") + settings._usage.influxdb_values_rotate() def start_web_server() -> None: @@ -93,21 +108,41 @@ def main() -> None: # print(settings._config) # print(settings._log) # print(settings._printer) - # print(settings._db) + # print(settings._local_db) + # print(settings._influx_db) # print(settings._files) # print(settings._web_server) #### #### DEBUG END #### - ## DELETE EXPIRED FILES IN BACKGROUND + #### + #### DEBUG INFLUXDB + #### + # settings._usage.influxdb_values_reset() + # import pdb; pdb.set_trace() + + ### SET SCHEDULER scheduler = BackgroundScheduler() + + ### DELETE EXPIRED FILES IN BACKGROUND scheduler.add_job( files_cleaner, 'interval', minutes=settings._config.keep_file_minutes ) logger("Starting background deleting expired files...") + + ### ROTATE VALUES ON THE INFLUXDB + if settings._config.enabled_influxdb: + scheduler.add_job( + rotate_influxdb_values, + 'interval', + minutes=settings._influx_db.rotation_minutes + ) + logger("Starting background rotating InfluxDB values...") + + ### START SCHEDULER scheduler.start() ### START THE APP diff --git a/src/requirements.txt b/src/requirements.txt index 7f4bde1cd21afb854709ff7dc5b09a72e2b7009c..aed3caac38f299789466ed917d65af0b50a561a3 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -8,11 +8,13 @@ tinydb APScheduler pytz pypdf +### FLASK flask flask-login -# Flask -# Flask-SQLAlchemy -# Flask-Login +### API +flask_jwt_extended +### INFLUXDB +influxdb-client ### TESTS # pylint # pep8 diff --git a/src/settings.py b/src/settings.py index 6609e789d9ea1ccfcc0c9c87bfaa22f9d2312f55..c155423a51ecc362bb9e71541cb6bb098f438095 100644 --- a/src/settings.py +++ b/src/settings.py @@ -52,15 +52,25 @@ _log = set_logger(config=_config) _printer = utils.AppPrinter(config=_config) ### DB -_db = utils.AppDB(config=_config) +_local_db = utils.AppLocalDB(config=_config) ### FILES _files = utils.AppFiles(config=_config) +### INFLUXDB +if _config.enabled_influxdb: + _influx_db = utils.AppInfluxDB(config=_config) +else: + _influx_db = None + +### USAGE +_usage = utils.AppUsage(config=_config, influxdb=_influx_db) + ### FLASK _web_server = web.AppServer( config=_config, printer=_printer, - db_files=_db, - files=_files + db_files=_local_db, + files=_files, + usage=_usage ) diff --git a/src/tests/files/2025_01_17_Graphana.png b/src/tests/files/2025_01_17_Graphana.png new file mode 100644 index 0000000000000000000000000000000000000000..99477a02f431c169ceda7ce9bd494ad72f17080f Binary files /dev/null and b/src/tests/files/2025_01_17_Graphana.png differ diff --git a/src/tests/files/2025_01_17_Graphana_2.png b/src/tests/files/2025_01_17_Graphana_2.png new file mode 100644 index 0000000000000000000000000000000000000000..4b0409ebfd2965514ec49bf5148aee510f28acab Binary files /dev/null and b/src/tests/files/2025_01_17_Graphana_2.png differ diff --git a/src/tests/utils/test_app_db.py b/src/tests/utils/test_app_db.py deleted file mode 100644 index 48c92a67b57f26d8eb0f320ca5b780d390cc5edb..0000000000000000000000000000000000000000 --- a/src/tests/utils/test_app_db.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -""" - tests.test_app_db.py file for the c3 printing system. - File for testing the modules and functions from the utils.app_db.py file. -""" - -import unittest -from unittest.mock import patch, MagicMock - -### LOCAL -from utils.app_db import AppDB - -### -### VARIABLES -### -TEST_PATH_TO_DATA = '/tmp' - - -class TestAppDB(unittest.TestCase): - """ Class for testing the utils.AppDB methods. - """ - - @patch('utils.app_db.TinyDB') - def setUp(self, mock_tinydb): - """ Method for setup the test class. - """ - mock_config = MagicMock() - # Config setup - mock_config.path_to_data = TEST_PATH_TO_DATA - mock_config.use_mem_db = False - mock_config.keep_file_minutes = 30 - mock_config.date_format = "%Y-%m-%d" - - # Create AppDB instance - self.app_db = AppDB(mock_config) - - # Access the mocked TinyDB instance - self.mock_db = mock_tinydb.return_value - - @patch('utils.app_db.TinyDB') - @patch('utils.app_db.CachingMiddleware') - def test_set_db_mem(self, mock_caching_middleware, mock_tinydb): - """ Tests the AppDb as using the db at the memory and not at local file. - """ - self.app_db.config.use_mem_db = True - self.app_db.set_db() - - # Check if TinyDB was initialized with caching - mock_tinydb.assert_called_once_with( - TEST_PATH_TO_DATA + '/db.json', - storage=mock_caching_middleware.return_value - ) - - @patch('utils.app_db.TinyDB') # Ensure TinyDB is mocked at the class level - def test_set_db_file(self, mock_tinydb): - """ Tests the AppDb as using the db at local file. - """ - self.app_db.config.use_mem_db = False - self.app_db.set_db() - - # Check if TinyDB was called once with the correct path - mock_tinydb.assert_called_once_with(TEST_PATH_TO_DATA + '/db.json') - - def test_get_all(self): - """ Tests utils.app_db.get_all() method. - """ - expected_result = [{'file_code': 'test_code'}] - self.mock_db.all.return_value = expected_result - - result = self.app_db.get_all() - self.assertEqual(result, expected_result) - - def test_get_file_info(self): - """ Tests utils.app_db.get_file_info(file_core: str) method. - """ - expected_info = {'file_code': 'test_code'} - self.mock_db.search.return_value = [expected_info] - - result = self.app_db.get_file_info('test_code') - self.assertEqual(result, expected_info) - - def test_get_file_info_multiple_records(self): - """ Tests utils.app_db.get_file_info(file_core: str) method from multiple records. - """ - # Mocking a case where multiple records are found - duplicate_info = [{'file_code': 'test_code'}, {'file_code': 'test_code'}] - self.mock_db.search.return_value = duplicate_info - - result = self.app_db.get_file_info('test_code') - self.assertIn('error_message', result) # We expect an error message when multiple records are found - - def test_file_save(self): - """ Tests utils.app_db.file_save(file_info) method. - """ - file_info = {'file_code': 'test_code'} - self.mock_db.insert = MagicMock() - - # Attempt to save the file information - self.app_db.file_save(file_info) - self.mock_db.insert.assert_called_once_with(file_info) - - def test_file_delete(self): - """ Tests utils.app_db.file_delete(file_info) method. - """ - file_info = {'file_code': 'test_code'} - self.mock_db.remove = MagicMock() - - # Call the delete method - self.app_db.file_delete(file_info) - self.mock_db.remove.assert_called_once() - self.assertEqual(self.mock_db.remove.call_args[0][0], self.app_db.query.file_code == 'test_code') - - def test_remove_all_files(self): - """ Tests utils.app_db.remove_all_files() method. - """ - self.mock_db.drop_tables = MagicMock() - - # Call the method to remove all files - self.app_db.remove_all_files() - self.mock_db.drop_tables.assert_called_once() - - -if __name__ == '__main__': - unittest.main() diff --git a/src/tests/utils/test_app_printer.py b/src/tests/utils/test_app_printer.py index 9e9cc13443e506af6199f257ee3c20c106095c07..5bcf4c6d0a47078c48e51efe83ca483e0ef53595 100644 --- a/src/tests/utils/test_app_printer.py +++ b/src/tests/utils/test_app_printer.py @@ -22,6 +22,7 @@ class TestAppPrinter(unittest.TestCase): # Mock the config object mock_config = MagicMock() mock_config.printer_name = 'Test Printer' + mock_config.debug_disable_printing = False # Create instance of AppPrinter app_printer = AppPrinter(mock_config) @@ -54,6 +55,7 @@ class TestAppPrinter(unittest.TestCase): """ mock_config = MagicMock() mock_config.printer_name = 'Test Printer' + mock_config.debug_disable_printing = False app_printer = AppPrinter(mock_config) file_path = 'test_document.txt' diff --git a/src/tests/web/test_server.py b/src/tests/web/test_server.py index f557616b22c68aee869441d1d4a1ab07e59ee7cc..d8d5f99fc1f6b5a1ae19587ce61680eedb471362 100644 --- a/src/tests/web/test_server.py +++ b/src/tests/web/test_server.py @@ -49,6 +49,11 @@ class TestAppServer(unittest.TestCase): self.mock_config.max_upload_bytes_folder = 10000000000 self.mock_config.num_digits = 6 self.mock_config.debug = True + ### + self.mock_config.verbose = True + self.mock_config.log_format = "%(asctime)s - %(levelname)s - %(module)s - %(funcName)s - %(message)s" + self.mock_config.log_to_file = False + self.mock_config.log_file_mode = "append" self.mock_config.num_digits = 6 self.mock_config.allowed_file_extensions = "{'txt', 'jpg', 'png', 'pdf'}" @@ -61,6 +66,7 @@ class TestAppServer(unittest.TestCase): self.mock_printer = MagicMock() self.mock_db_files = MagicMock() self.mock_files = AppFiles(config=self.mock_config) + self.mock_usage = MagicMock() # Create an instance of AppServer self.server = AppServer( @@ -68,6 +74,7 @@ class TestAppServer(unittest.TestCase): printer=self.mock_printer, db_files=self.mock_db_files, files=self.mock_files, + usage=self.mock_usage ) # Set up Flask test client @@ -283,13 +290,20 @@ class TestAppServer(unittest.TestCase): ### ### PROTECTED ### - @patch('utils.app_usage.AppUsage.counter_max_simultaneous_uploaded_get') - @patch('utils.app_usage.AppUsage.counter_pages_get') - @patch('utils.app_usage.AppUsage.counter_uploads_get') - @patch('utils.app_usage.AppUsage.counter_prints_get') - def test_home_admin_route_login(self, _, __, ___, ____): + @patch('web.routes_protected.AppRoutesProtected._get_counters_home_admin') + def test_home_admin_route_login(self, mock_get_counters): """ Test the home admin route. """ + ### Set mock + mock_get_counters.return_value = { + 'tot_uploads': 1, + 'tot_prints': 1, + 'tot_pages': 1, + 'tot_files_on_folder': 1, + 'tot_size': 1, + 'max_simultaneous_uploaded': 1, + } + # Test that the /home-admin route is accessible when logged in with self.client: # Simulate a login for the test @@ -297,6 +311,7 @@ class TestAppServer(unittest.TestCase): 'email': self.mock_config.login_user_email, 'password': self.mock_config.login_user_passwd }) + # import pdb; pdb.set_trace() response = self.client.get('/home-admin') # Expecting OK status self.assertEqual(response.status_code, 200) @@ -312,9 +327,16 @@ class TestAppServer(unittest.TestCase): # Expecting Unauthorized status self.assertEqual(response.status_code, 401) - def test_uploaded_list_route_login(self): + @patch('web.routes_protected.AppRoutesProtected._get_counters_uploaded_list') + def test_uploaded_list_route_login(self, mock_get_counters): """ Test the home admin route. """ + ### Set mock + mock_get_counters.return_value = { + 'tot_uploaded_files': 1, + 'tot_folder_size': 1, + } + # Test that the /home-admin route is accessible when logged in with self.client: # Simulate a login for the test @@ -496,7 +518,7 @@ class TestAppServer(unittest.TestCase): self.assertEqual(response.status_code, 401) def test_debug_admin_route_login_admin(self): - """ Test the /debug-admin route logged in. + """ Test the /debug-printers route logged in. """ with self.client: # Simulate a successful login @@ -505,11 +527,11 @@ class TestAppServer(unittest.TestCase): 'password': self.mock_config.login_admin_passwd }) - response = self.client.get('/debug-admin') + response = self.client.get('/debug-printers') # expect OK self.assertEqual(response.status_code, 200) # Assuming 'h1' in the home page content - page_h1 = DEFAULT_ROUTE_VALS.get('DEBUG_ADMIN').get('page_h1') + page_h1 = DEFAULT_ROUTE_VALS.get('DEBUG_PRINTERS').get('page_h1') self.assertIn(page_h1.encode('utf-8'), response.data) def test_debug_admin_route_login_user(self): @@ -522,7 +544,7 @@ class TestAppServer(unittest.TestCase): 'password': self.mock_config.login_user_passwd }) - response = self.client.get('/debug-admin') + response = self.client.get('/debug-printers') # expect not allowed self.assertEqual(response.status_code, 403) @@ -530,7 +552,7 @@ class TestAppServer(unittest.TestCase): """ Test the /debug-admin route logged out. """ # Test that the /debug-admin route is not accessible when not logged in - response = self.client.get('/debug-admin') + response = self.client.get('/debug-printers') # Expecting Unauthorized status self.assertEqual(response.status_code, 401) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 7d410838930821829b4347e4248b58904bbff6fc..4f5c5de900d6003619ce5d1286d5bdde4b1cc328 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -5,7 +5,8 @@ from utils.app_configuration import AppConfiguration -from utils.app_db import AppDB +from utils.app_influxdb import AppInfluxDB +from utils.app_local_db import AppLocalDB from utils.app_files import AppFiles from utils.app_printer import AppPrinter from utils.app_questions import AppQuestions diff --git a/src/utils/app_configuration.py b/src/utils/app_configuration.py index 94259dc3ada757583c2fc83639f24d460bf1834f..3024c64e87a97e16bcc5cf5f2c337d23068d25e1 100644 --- a/src/utils/app_configuration.py +++ b/src/utils/app_configuration.py @@ -58,6 +58,22 @@ class AppConfiguration(): self.from_parsed = self.configuration_from_parsed() + ### SET ATTRS + self.__set_my_attr__() + + ### PRINT + if self.debug: + self.__debug_attrs__() + + def __set_my_attr__(self) -> None: + """ + Method for setting the class attributes. + + Returns + ------- + None + """ + ### SET ATTRS self.printer_name = self.from_parsed.printer_name self.port_server = int(self.from_parsed.port_server) @@ -70,7 +86,10 @@ class AppConfiguration(): self.login_user_passwd = self.from_parsed.login_user_passwd self.login_admin_email = self.from_parsed.login_admin_email self.login_admin_passwd = self.from_parsed.login_admin_passwd + self.login_api_email = self.from_parsed.login_api_email + self.login_api_passwd = self.from_parsed.login_api_passwd self.debug = self.from_parsed.debug + self.debug_disable_printing = self.from_parsed.debug_disable_printing self.verbose = self.from_parsed.verbose self.log_to_file = self.from_parsed.log_to_file self.log_file_mode = self.from_parsed.log_file_mode @@ -86,18 +105,26 @@ class AppConfiguration(): self.use_mem_db = self.from_parsed.use_mem_db self.enabled_questions = self.from_parsed.enabled_questions self.enabled_about = self.from_parsed.enabled_about + self.enabled_api = self.from_parsed.enabled_api + self.enabled_influxdb = self.from_parsed.enabled_influxdb self.enabled_print_with_options = self.from_parsed.enabled_print_with_options self.file_preview_width = int(self.from_parsed.file_preview_width) self.file_preview_height = int(self.from_parsed.file_preview_height) self.file_auto_delete_after_print = self.from_parsed.file_auto_delete_after_print - - ### PRINT - if self.debug: - self.__debug_attrs__() + self.api_token_expire_hours = self.from_parsed.api_token_expire_hours + self.influxdb_token = self.from_parsed.influxdb_token + self.influxdb_organization = self.from_parsed.influxdb_organization + self.influxdb_port = self.from_parsed.influxdb_port + self.influxdb_url = self.from_parsed.influxdb_url + self.influxdb_measurement = self.from_parsed.influxdb_measurement + self.influxdb_rotation_minutes = self.from_parsed.influxdb_rotation_minutes + self.influxdb_bucket = self.from_parsed.influxdb_bucket + self.influxdb_bucket_description = self.from_parsed.influxdb_bucket_description + self.influxdb_bucket_retention_days = self.from_parsed.influxdb_bucket_retention_days def __configuration_colors__(self) -> None: """ - Method for setting the terminal output with colors + Method for setting the terminal output with colors. Returns ------- @@ -185,6 +212,13 @@ class AppConfiguration(): default_name='DEFAULT_DEBUG', default_val=DEFAULT_VALUES.get('DEFAULT_DEBUG') ) + ### BOOLEAN: DEBUG_DISABLE_PRINTING + ret_dict['debug_disable_printing'] = self.get_env_bool( + name_env='DEBUG_DISABLE_PRINTING', + name_config='debug_disable_printing', + default_name='DEFAULT_DEBUG_DISABLE_PRINTING', + default_val=DEFAULT_VALUES.get('DEFAULT_DEBUG_DISABLE_PRINTING') + ) ### BOOLEAN: VERBOSE ret_dict['verbose'] = self.get_env_bool( name_env='VERBOSE', @@ -220,6 +254,20 @@ class AppConfiguration(): default_name='DEFAULT_ENABLED_ABOUT', default_val=DEFAULT_VALUES.get('DEFAULT_ENABLED_ABOUT') ) + ### BOOLEAN: ENABLED_API + ret_dict['enabled_api'] = self.get_env_bool( + name_env='ENABLED_API', + name_config='enabled_api', + default_name='DEFAULT_ENABLED_API', + default_val=DEFAULT_VALUES.get('DEFAULT_ENABLED_API') + ) + ### BOOLEAN: ENABLED_INFLUXDB + ret_dict['enabled_influxdb'] = self.get_env_bool( + name_env='ENABLED_INFLUXDB', + name_config='enabled_influxdb', + default_name='DEFAULT_ENABLED_INFLUXDB', + default_val=DEFAULT_VALUES.get('DEFAULT_ENABLED_INFLUXDB') + ) ### BOOLEAN: ENABLED_PRINT_WITH_OPTIONS ret_dict['enabled_print_with_options'] = self.get_env_bool( name_env='ENABLED_PRINT_WITH_OPTIONS', @@ -304,6 +352,27 @@ class AppConfiguration(): default_name='DEFAULT_LOGIN_ADMIN_PASSWD', default_val=DEFAULT_VALUES.get('DEFAULT_LOGIN_ADMIN_PASSWD') ) + ### LOGIN_API_EMAIL + ret_dict['login_api_email'] = self.get_env( + name_env='LOGIN_API_EMAIL', + name_config='login_api_email', + default_name='DEFAULT_LOGIN_API_EMAIL', + default_val=DEFAULT_VALUES.get('DEFAULT_LOGIN_API_EMAIL') + ) + ### LOGIN_API_PASSWD + ret_dict['login_api_passwd'] = self.get_env( + name_env='LOGIN_API_PASSWD', + name_config='login_api_passwd', + default_name='DEFAULT_LOGIN_API_PASSWD', + default_val=DEFAULT_VALUES.get('DEFAULT_LOGIN_API_PASSWD') + ) + ### API_TOKEN_EXPIRE_HOURS + ret_dict['api_token_expire_hours'] = self.get_env( + name_env='API_TOKEN_EXPIRE_HOURS', + name_config='api_token_expire_hours', + default_name='DEFAULT_API_TOKEN_EXPIRE_HOURS', + default_val=DEFAULT_VALUES.get('DEFAULT_API_TOKEN_EXPIRE_HOURS') + ) ### LOG_MODE ret_dict['log_file_mode'] = self.get_env( name_env='LOG_MODE', @@ -395,6 +464,69 @@ class AppConfiguration(): default_name='DEFAULT_FILE_PREVIEW_HEIGHT', default_val=DEFAULT_VALUES.get('DEFAULT_FILE_PREVIEW_HEIGHT') ) + ### INFLUXDB_TOKEN + ret_dict['influxdb_token'] = self.get_env( + name_config='influxdb_token', + name_env='INFLUXDB_TOKEN', + default_name='DEFAULT_INFLUXDB_TOKEN', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_TOKEN') + ) + ### INFLUXDB_ORGANIZATION + ret_dict['influxdb_organization'] = self.get_env( + name_config='influxdb_organization', + name_env='INFLUXDB_ORGANIZATION', + default_name='DEFAULT_INFLUXDB_ORGANIZATION', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_ORGANIZATION') + ) + ### INFLUXDB_PORT + ret_dict['influxdb_port'] = self.get_env( + name_config='influxdb_port', + name_env='INFLUXDB_PORT', + default_name='DEFAULT_INFLUXDB_PORT', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_PORT') + ) + ### INFLUXDB_URL + ret_dict['influxdb_url'] = self.get_env( + name_config='influxdb_url', + name_env='INFLUXDB_URL', + default_name='DEFAULT_INFLUXDB_URL', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_URL') + ) + ### INFLUXDB_MEASUREMENT + ret_dict['influxdb_measurement'] = self.get_env( + name_config='influxdb_measurement', + name_env='INFLUXDB_MEASUREMENT', + default_name='DEFAULT_INFLUXDB_MEASUREMENT', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_MEASUREMENT') + ) + ### INFLUXDB_ROTATION_MINUTES + ret_dict['influxdb_rotation_minutes'] = self.get_env( + name_config='influxdb_rotation_minutes', + name_env='INFLUXDB_ROTATION_MINUTES', + default_name='DEFAULT_INFLUXDB_ROTATION_MINUTES', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_ROTATION_MINUTES') + ) + ### INFLUXDB_BUCKET + ret_dict['influxdb_bucket'] = self.get_env( + name_config='influxdb_bucket', + name_env='INFLUXDB_BUCKET', + default_name='DEFAULT_INFLUXDB_BUCKET', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_BUCKET') + ) + ### INFLUXDB_BUCKET_DESCRIPTION + ret_dict['influxdb_bucket_description'] = self.get_env( + name_config='influxdb_bucket_description', + name_env='INFLUXDB_BUCKET_DESCRIPTION', + default_name='DEFAULT_INFLUXDB_BUCKET_DESCRIPTION', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_BUCKET_DESCRIPTION') + ) + ### INFLUXDB_BUCKET_RETENTION_DAYS + ret_dict['influxdb_bucket_retention_days'] = self.get_env( + name_config='influxdb_bucket_retention_days', + name_env='INFLUXDB_BUCKET_RETENTION_DAYS', + default_name='DEFAULT_INFLUXDB_BUCKET_RETENTION_DAYS', + default_val=DEFAULT_VALUES.get('DEFAULT_INFLUXDB_BUCKET_RETENTION_DAYS') + ) return ret_dict @@ -410,221 +542,326 @@ class AppConfiguration(): parser = argparse.ArgumentParser(description='c3 InfoDesk Printer') ### BOOLEAN: DEBUG parser.add_argument( - '-d', '--debug', + '--debug', help='Enable debugging information from execution.', default=self.from_env.get('debug'), action='store_true' ) + ### BOOLEAN: DEBUG + parser.add_argument( + '--debug_disable_printing', + help='Disable the printing funcionallity for debugging.', + default=self.from_env.get('debug_disable_printing'), + action='store_true' + ) ### BOOLEAN: VERBOSE parser.add_argument( - '-v', '--verbose', + '--verbose', help='Enable printing information from execution.', default=self.from_env.get('verbose'), action='store_true' ) ### BOOLEAN: LOG TO FILE parser.add_argument( - '-lf', '--log_to_file', + '--log_to_file', help='Enable writing the log to a file.', default=self.from_env.get('log_to_file'), action='store_true' ) ### BOOLEAN: USE MEM DB parser.add_argument( - '-umd', '--use_mem_db', + '--use_mem_db', help='If using the TinyDB in memory or file.', default=self.from_env.get('use_mem_db'), action='store_true' ) ### BOOLEAN: ENABLED QUESTIONS parser.add_argument( - '-eq', '--enabled_questions', + '--enabled_questions', help='If using the questions functionality.', default=self.from_env.get('enabled_questions'), action='store_true' ) ### BOOLEAN: ENABLED ABOUT parser.add_argument( - '-ea', '--enabled_about', + '--enabled_about', help='If using the about functionality.', default=self.from_env.get('enabled_about'), action='store_true' ) ### BOOLEAN: ENABLED PRINT WITH OPTIONS parser.add_argument( - '-epwf', '--enabled_print_with_options', + '--enabled_print_with_options', help='If using the functionality for printing with options.', default=self.from_env.get('enabled_print_with_options'), action='store_true' ) + ### BOOLEAN: ENABLED API + parser.add_argument( + '--enabled_api', + help='If using the API functionality.', + default=self.from_env.get('enabled_api'), + action='store_true' + ) + ### BOOLEAN: ENABLED API + parser.add_argument( + '--enabled_influxdb', + help='If using the InfluxDB functionality.', + default=self.from_env.get('enabled_influxdb'), + action='store_true' + ) ### BOOLEAN: DEFAULT FILE AUTO DELETE AFTER PRINT parser.add_argument( - '-fadap', '--file_auto_delete_after_print', + '--file_auto_delete_after_print', help='If auto deleting the file after printing.', default=self.from_env.get('file_auto_delete_after_print'), action='store_true' ) ### PATH TO UPLOAD parser.add_argument( - '-pu', '--path_to_upload', + '--path_to_upload', help='Path where the printing files should be uploaded.', default=self.from_env.get('path_to_upload'), type=str ) ### PATH TO LOG FILE parser.add_argument( - '-pl', '--path_to_log', + '--path_to_log', help='Path where the log file should be written.', default=self.from_env.get('path_to_log'), type=str ) ### PATH TO DATA parser.add_argument( - '-pd', '--path_to_data', + '--path_to_data', help='Path where the app data should be written.', default=self.from_env.get('path_to_data'), type=str ) ### PATH TO TEST FILE parser.add_argument( - '-pt', '--path_test_file', + '--path_test_file', help='Path where the test print file location.', default=self.from_env.get('path_to_test_file'), type=str ) ### SERVER PORT parser.add_argument( - '-p', '--port_server', + '--port_server', help='The port of the system', default=self.from_env.get('server_port'), type=int ) ### SERVER SECRET KEY parser.add_argument( - '-ssc', '--server_secret_key', + '--server_secret_key', help='The secret key from the server', default=self.from_env.get('server_secret_key'), type=str ) ### LOG FILE MODE parser.add_argument( - '-lfm', '--log_file_mode', + '--log_file_mode', help="Mode that the log file should be written, it can be: 'append' or 'new'.", default=self.from_env.get('log_file_mode'), type=str ) ### LOG FORMAT parser.add_argument( - '-f', '--log_format', + '--log_format', help='Log format to be used.', default=self.from_env.get('log_format'), type=str ) ### PRINTER NAME parser.add_argument( - '-pn', '--printer_name', + '--printer_name', help='Name of the printer to be used.', default=self.from_env.get('printer_name'), type=str ) ### LOGIN USER EMAIL parser.add_argument( - '-lue', '--login_user_email', + '--login_user_email', help='Email to be used for login as user for printing.', default=self.from_env.get('login_user_email'), type=str ) ### LOGIN USER PASSWORD parser.add_argument( - '-lup', '--login_user_passwd', + '--login_user_passwd', help='Password to be used for login as user for printing.', default=self.from_env.get('login_user_passwd'), type=str ) ### LOGIN ADMIN EMAIL parser.add_argument( - '-lae', '--login_admin_email', + '--login_admin_email', help='Email to be used for login as admin.', default=self.from_env.get('login_admin_email'), type=str ) ### LOGIN ADMIN PASSWORD parser.add_argument( - '-lap', '--login_admin_passwd', + '--login_admin_passwd', help='Password to be used for login as admin.', default=self.from_env.get('login_admin_passwd'), type=str ) + ### LOGIN API EMAIL + parser.add_argument( + '--login_api_email', + help='Email to be used for login as api.', + default=self.from_env.get('login_api_email'), + type=str + ) + ### LOGIN API PASSWORD + parser.add_argument( + '--login_api_passwd', + help='Password to be used for login as api.', + default=self.from_env.get('login_api_passwd'), + type=str + ) ### KEEP_ALIVE_MIN parser.add_argument( - '-ka', '--keep_alive', + '--keep_alive', help='The amount of minutes to keep the file at the server before deletion.', default=self.from_env.get('keep_file_minutes'), type=str ) ### NUM_DIGITS parser.add_argument( - '-nd', '--num_digits', + '--num_digits', help='The lenght of the generated code from the file to be printed.', default=self.from_env.get('num_digits'), type=int ) ### MAX_UPLOAD_FOLDER_BYTES parser.add_argument( - '-muf', '--max_upload_bytes_folder', + '--max_upload_bytes_folder', help='The maximum size in bytes that can be upload folder can keep.', default=self.from_env.get('max_upload_bytes_folder'), type=int ) ### MAX_UPLOAD_FILE_BYTES parser.add_argument( - '-mu', '--max_upload_bytes_file', + '--max_upload_bytes_file', help='The maximum size in bytes from a file that can be uploaded.', default=self.from_env.get('max_upload_bytes_file'), type=int ) ### MAX_UPLOAD_PDF_PAGES parser.add_argument( - '-mupg', '--max_upload_pdf_pages', + '--max_upload_pdf_pages', help='The maximum number of pages from a PDF file that can be uploaded.', default=self.from_env.get('max_upload_pdf_pages'), type=int ) ### MAX_UPLOAD_FILE_COPIES parser.add_argument( - '-mufc', '--max_upload_file_copies', + '--max_upload_file_copies', help='The maximum number of copies from a file that can be printed.', default=self.from_env.get('max_upload_file_copies'), type=int ) ### DATE_FORMAT parser.add_argument( - '-df', '--date_format', + '--date_format', help='The date format to be used handiling the system.', default=str(self.from_env.get('date_format')), type=str ) ### ALLOWED_FILE_EXTENSIONS parser.add_argument( - '-afe', '--allowed_file_extensions', + '--allowed_file_extensions', help='The allowed file extensions that the system can use.', default=str(self.from_env.get('allowed_file_extensions')), type=str ) ### FILE PREVIEW WIDTH parser.add_argument( - '-fpw', '--file_preview_width', + '--file_preview_width', help='The width at the webpage for previewing a file.', default=str(self.from_env.get('file_preview_width')), type=str ) ### FILE PREVIEW HEIGHT parser.add_argument( - '-fph', '--file_preview_height', + '--file_preview_height', help='The height at the webpage for previewing a file.', default=str(self.from_env.get('file_preview_height')), type=str ) + ### API_TOKEN_EXPIRE_HOURS + parser.add_argument( + '--api_token_expire_hours', + help='How many hours till the API token to expire.', + default=self.from_env.get('api_token_expire_hours'), + type=int + ) + ### INFLUXDB_TOKEN + parser.add_argument( + '--influxdb_token', + help='The token to be used for InfluxDB.', + default=self.from_env.get('influxdb_token'), + type=str + ) + ### INFLUXDB_ORGANIZATION + parser.add_argument( + '--influxdb_organization', + help='The organization to be used for InfluxDB.', + default=self.from_env.get('influxdb_organization'), + type=str + ) + ### INFLUXDB_PORT + parser.add_argument( + '--influxdb_port', + help='The port of the InfluxDB server.', + default=self.from_env.get('influxdb_port'), + type=str + ) + ### INFLUXDB_URL + parser.add_argument( + '--influxdb_url', + help='The URL of the InfluxDB server.', + default=self.from_env.get('influxdb_url'), + type=str + ) + ### INFLUXDB_MEASUREMENT + parser.add_argument( + '--influxdb_measurement', + help='The measurement name of the InfluxDB server.', + default=self.from_env.get('influxdb_measurement'), + type=str + ) + ### INFLUXDB_ROTATION_MINUTES + parser.add_argument( + '--influxdb_rotation_minutes', + help='how many minutes to refresh the values at the InfluxDB server.', + default=self.from_env.get('influxdb_rotation_minutes'), + type=str + ) + ### INFLUXDB_BUCKET + parser.add_argument( + '--influxdb_bucket', + help='The bucket name of the InfluxDB server.', + default=self.from_env.get('influxdb_bucket'), + type=str + ) + ### INFLUXDB_BUCKET_DESCRIPTION + parser.add_argument( + '--influxdb_bucket_description', + help='The description from the bucket of the InfluxDB server.', + default=self.from_env.get('influxdb_bucket_description'), + type=str + ) + ### INFLUXDB_BUCKET_RETENTION_DAYS + parser.add_argument( + '--influxdb_bucket_retention_days', + help='The retention days from the bucket of the InfluxDB server.', + default=self.from_env.get('influxdb_bucket_retention_days'), + type=str + ) return parser.parse_args() @@ -649,7 +886,10 @@ class AppConfiguration(): 'login_user_passwd': self.login_user_passwd, 'login_admin_email': self.login_admin_email, 'login_admin_passwd': self.login_admin_passwd, + 'login_api_email': self.login_api_email, + 'login_api_passwd': self.login_api_passwd, 'debug': self.debug, + 'debug_disable_printing': self.debug_disable_printing, 'verbose': self.verbose, 'log_to_file': self.log_to_file, 'log_file_mode': self.log_file_mode, @@ -665,10 +905,22 @@ class AppConfiguration(): 'use_mem_db': self.use_mem_db, 'enabled_questions': self.enabled_questions, 'enabled_about': self.enabled_about, + 'enabled_api': self.enabled_api, + 'enabled_influxdb': self.enabled_influxdb, 'enabled_print_with_options': self.enabled_print_with_options, 'file_preview_width': self.file_preview_width, 'file_preview_height': self.file_preview_height, - 'file_auto_delete_after_print': self.file_auto_delete_after_print + 'file_auto_delete_after_print': self.file_auto_delete_after_print, + 'api_token_expire_hours': self.api_token_expire_hours, + 'influxdb_token': self.influxdb_token, + 'influxdb_organization': self.influxdb_organization, + 'influxdb_port': self.influxdb_port, + 'influxdb_url': self.influxdb_url, + 'influxdb_measurement': self.influxdb_measurement, + 'influxdb_rotation_minutes': self.influxdb_rotation_minutes, + 'influxdb_bucket': self.influxdb_bucket, + 'influxdb_bucket_description': self.influxdb_bucket_description, + 'influxdb_bucket_retention_days': self.influxdb_bucket_retention_days, } def __debug_attrs__(self) -> None: @@ -691,7 +943,10 @@ class AppConfiguration(): print(self.color_green + "\t- login_user_passwd: " + self.color_end + f"'{self.login_user_passwd}'") print(self.color_green + "\t- login_admin_email: " + self.color_end + f"'{self.login_admin_email}'") print(self.color_green + "\t- login_admin_passwd: " + self.color_end + f"'{self.login_admin_passwd}'") + print(self.color_green + "\t- login_api_email: " + self.color_end + f"'{self.login_api_email}'") + print(self.color_green + "\t- login_api_passwd: " + self.color_end + f"'{self.login_api_passwd}'") print(self.color_green + "\t- debug: " + self.color_end + f"'{self.debug}'") + print(self.color_green + "\t- debug_disable_printing: " + self.color_end + f"'{self.debug_disable_printing}'") print(self.color_green + "\t- verbose: " + self.color_end + f"'{self.verbose}'") print(self.color_green + "\t- log_to_file: " + self.color_end + f"'{self.log_to_file}'") print(self.color_green + "\t- log_file_mode: " + self.color_end + f"'{self.log_file_mode}'") @@ -707,8 +962,20 @@ class AppConfiguration(): print(self.color_green + "\t- use_mem_db: " + self.color_end + f"'{self.use_mem_db}'") print(self.color_green + "\t- enabled_questions: " + self.color_end + f"'{self.enabled_questions}'") print(self.color_green + "\t- enabled_about: " + self.color_end + f"'{self.enabled_about}'") + print(self.color_green + "\t- enabled_api: " + self.color_end + f"'{self.enabled_api}'") + print(self.color_green + "\t- enabled_influxdb: " + self.color_end + f"'{self.enabled_influxdb}'") print(self.color_green + "\t- enabled_print_with_options: " + self.color_end + f"'{self.enabled_print_with_options}'") print(self.color_green + "\t- file_preview_width: " + self.color_end + f"'{self.file_preview_width}'") print(self.color_green + "\t- file_preview_height: " + self.color_end + f"'{self.file_preview_height}'") print(self.color_green + "\t- file_auto_delete_after_print: " + self.color_end + f"'{self.file_auto_delete_after_print}'") + print(self.color_green + "\t- api_token_expire_hours: " + self.color_end + f"'{self.api_token_expire_hours}'") + print(self.color_green + "\t- influxdb_token: " + self.color_end + f"'{self.influxdb_token}'") + print(self.color_green + "\t- influxdb_organization: " + self.color_end + f"'{self.influxdb_organization}'") + print(self.color_green + "\t- influxdb_port: " + self.color_end + f"'{self.influxdb_port}'") + print(self.color_green + "\t- influxdb_url: " + self.color_end + f"'{self.influxdb_url}'") + print(self.color_green + "\t- influxdb_measurement: " + self.color_end + f"'{self.influxdb_measurement}'") + print(self.color_green + "\t- influxdb_rotation_minutes: " + self.color_end + f"'{self.influxdb_rotation_minutes}'") + print(self.color_green + "\t- influxdb_bucket: " + self.color_end + f"'{self.influxdb_bucket}'") + print(self.color_green + "\t- influxdb_bucket_description: " + self.color_end + f"'{self.influxdb_bucket_description}'") + print(self.color_green + "\t- influxdb_bucket_retention_days: " + self.color_end + f"'{self.influxdb_bucket_retention_days}'") print("\n") diff --git a/src/utils/app_default_vals.py b/src/utils/app_default_vals.py index 5d2acd28fe6376d0e6923d26165025a27e45c301..ead362fffd1aaef0225f90bea8fe5a9384bb3cdf 100644 --- a/src/utils/app_default_vals.py +++ b/src/utils/app_default_vals.py @@ -21,7 +21,10 @@ DEFAULT_VALUES = { 'DEFAULT_LOGIN_USER_PASSWD': 'Please set the login password', 'DEFAULT_LOGIN_ADMIN_EMAIL': 'Please set the login email', 'DEFAULT_LOGIN_ADMIN_PASSWD': 'Please set the login password', + 'DEFAULT_LOGIN_API_EMAIL': 'Please set the login email', + 'DEFAULT_LOGIN_API_PASSWD': 'Please set the login password', 'DEFAULT_DEBUG': False, + 'DEFAULT_DEBUG_DISABLE_PRINTING': False, 'DEFAULT_VERBOSE': False, 'DEFAULT_LOG_TO_FILE': False, 'DEFAULT_LOG_MODE': 'new', @@ -35,10 +38,47 @@ DEFAULT_VALUES = { 'DEFAULT_USE_MEM_DB': False, 'DEFAULT_ENABLED_QUESTIONS': False, 'DEFAULT_ENABLED_ABOUT': False, + 'DEFAULT_ENABLED_API': False, + 'DEFAULT_ENABLED_INFLUXDB': False, 'DEFAULT_ENABLED_PRINT_WITH_OPTIONS': False, 'DEFAULT_FILE_PREVIEW_WIDTH': '800', 'DEFAULT_FILE_PREVIEW_HEIGHT': '600', 'DEFAULT_FILE_AUTO_DELETE_AFTER_PRINT': False, 'DEFAULT_MAX_UPLOAD_PDF_PAGES': 10, - 'DEFAULT_MAX_UPLOAD_FILE_COPIES': 5 + 'DEFAULT_MAX_UPLOAD_FILE_COPIES': 5, + 'DEFAULT_API_TOKEN_EXPIRE_HOURS': 120, + 'DEFAULT_INFLUXDB_TOKEN': 'Please set the influxdb token', + 'DEFAULT_INFLUXDB_PORT': 8086, + 'DEFAULT_INFLUXDB_URL': 'http://localhost', + 'DEFAULT_INFLUXDB_ORGANIZATION': 'c3infodesk', + 'DEFAULT_INFLUXDB_MEASUREMENT': 'app-usage', + 'DEFAULT_INFLUXDB_ROTATION_MINUTES': 2, + 'DEFAULT_INFLUXDB_BUCKET': 'c3infodesk-print', + 'DEFAULT_INFLUXDB_BUCKET_DESCRIPTION': 'Bucket used from InfoDesk at the 39c3', + 'DEFAULT_INFLUXDB_BUCKET_RETENTION_DAYS': 10, + 'DEFAULT_INFLUXDB_FIELD_NAME_UPLOADS': 'uploads', + 'DEFAULT_INFLUXDB_FIELD_NAME_FILES': 'files', + 'DEFAULT_INFLUXDB_FIELD_NAME_PRINTS': 'prints', + 'DEFAULT_INFLUXDB_FIELD_NAME_PAGES': 'pages', + 'DEFAULT_INFLUXDB_FIELD_NAME_SIZE': 'size', + 'DEFAULT_INFLUXDB_FIELD_NAME_SIMULTANEOUS_UPLOADS': 'simultaneous_uploads', + 'DEFAULT_INFLUXDB_BUCKET_LABELS': [ + { + 'name': '39c3', + 'description': '', + 'color': '#be2ee4' + }, + { + 'name': '39c3_InfoDesk_Printer', + 'description': 'InfoDesk Printer at 39c3', + 'color': '#be2ee4' + } + ], + 'DEFAULT_PRINTING_WITH_OPTIONS_VALUES': { + 'print_media': 'A4', + 'print_color': 'disable', + 'print_sides': 'single', + 'print_copies': '1', + 'print_ranges': 'all', + } } diff --git a/src/utils/app_files.py b/src/utils/app_files.py index 5d48159b59144db0d992b51c8f4d5628288a2ca6..66993f2e47e261c2bce86262318bb15767306b60 100644 --- a/src/utils/app_files.py +++ b/src/utils/app_files.py @@ -148,6 +148,13 @@ class AppFiles(): new_file_name = file_code + '___-___' + file_date + '___-___' + filename file_path = self.config.path_to_upload + '/' + new_file_name + # return { + # 'file_code': file_code, + # 'uploaded_date': file_date, + # 'original_name': filename, + # 'new_file_name': new_file_name, + # 'path': file_path + # } return { 'file_code': file_code, 'uploaded_date': file_date, @@ -191,6 +198,9 @@ class AppFiles(): ret_msg = f"The PDF exceeds the maximum allowed page count of '{self.config.max_upload_pdf_pages}'." raise Exception(ret_msg) + # File size + uploaded_file_info['size'] = file.tell() + # Add num_pages to uploaded_file_info uploaded_file_info['num_pages'] = num_pages @@ -222,9 +232,6 @@ class AppFiles(): uploaded_file_info = self._get_return_uploaded_file_info(file) - # Add num_pages to uploaded_file_info - uploaded_file_info['num_pages'] = 1 - try: file.save(uploaded_file_info.get('path')) # print("----> FILE SAVED") @@ -236,6 +243,12 @@ class AppFiles(): 'error_message': msg_err } + # File size + uploaded_file_info['size'] = file.tell() + + # Add num_pages to uploaded_file_info + uploaded_file_info['num_pages'] = 1 + logger(f"File with code: '{uploaded_file_info.get('file_code')}' saved to the Disk...") return uploaded_file_info diff --git a/src/utils/app_influxdb.py b/src/utils/app_influxdb.py new file mode 100644 index 0000000000000000000000000000000000000000..3278334595a542d31d05aa7ae357344058a66f2b --- /dev/null +++ b/src/utils/app_influxdb.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" + utils.app_influxdb.py file for the c3 printing system. + In this file it will be set the DB to be used at the system. +""" + +### +### DEPENDENCIES +### +import sys +from influxdb_client import InfluxDBClient, BucketRetentionRules, Point +from influxdb_client.client.write_api import SYNCHRONOUS + +### LOCAL +try: + from utils.app_logger import logger, logger_err +except ImportError as e: + print("\n\n Unable to import 'utils.app_logger.py' from the 'utils.app_influxdb.py' file from project \n\n") + sys.exit(1) + +try: + from utils.app_default_vals import DEFAULT_VALUES +except ImportError as e: + print("\n\n Unable to import 'web.routes_defaul_vals.py' from the 'utils.app_influxdb.py' file from project \n\n") + sys.exit(1) + + +class AppInfluxDB(): + """ + Class for setting the files InfluxDB of the app. + """ + + def __init__(self, config) -> None: + """ + Method for initialiazing of the AppInfluxDB class. + Sets the class variables. + + Returns + ------- + None + """ + self.config = config + + self.token = self.config.influxdb_token + self.org_name = self.config.influxdb_organization + self.bucket = self.config.influxdb_bucket + # Days * seconds + self.bucket_retention_days = self.config.influxdb_bucket_retention_days * 86400 + self.bucket_description = self.config.influxdb_bucket_description + self.measurement = self.config.influxdb_measurement + self.rotation_minutes = self.config.influxdb_rotation_minutes + self.port = self.config.influxdb_port + self.url = self.config.influxdb_url + ':' + self.config.influxdb_port + + # Set the bucket field names + self.field_name_uploads = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_UPLOADS') + self.field_name_files = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_FILES') + self.field_name_prints = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_PRINTS') + self.field_name_pages = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_PAGES') + self.field_name_size = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_SIZE') + self.field_name_simultaneous_uploads = DEFAULT_VALUES.get('DEFAULT_INFLUXDB_FIELD_NAME_SIMULTANEOUS_UPLOADS') + + self.set_client() + self.set_organization() + self.set_bucket() + self.set_labels() + + def set_client(self) -> None: + """ + Method for setting the InfluxDB client from the configuration. + + Returns + ------- + None + """ + logger("Setting the InfluxDB client...") + + try: + self.client = InfluxDBClient( + url=self.url, + token=self.token, + org=self.org_name + ) + + except Exception as e: + logger_err(f"Error setting the InfluxDB client with error: {str(e)}") + sys.exit(1) + + def set_organization(self) -> None: + """ + Method for setting the InfluxDB api_organizations from the configuration. + + Returns + ------- + None + """ + logger("Setting the InfluxDB api_organizations...") + + try: + self.api_organizations = self.client.organizations_api() + + except Exception as e: + logger_err(f"Error setting the InfluxDB 'organizations_api' with error: {str(e)}") + sys.exit(1) + + self.org_id = self.organization_check() + + def set_bucket(self) -> None: + """ + Method for setting the InfluxDB bucket from the configuration. + + Returns + ------- + None + """ + logger("Setting the InfluxDB bucket API...") + + try: + self.api_buckets = self.client.buckets_api() + + except Exception as e: + logger_err(f"Error setting the InfluxDB 'buckets_api' with error: {str(e)}") + sys.exit(1) + + self.bucket_check() + + def set_labels(self) -> None: + """ + Method for setting the InfluxDB api_lables from the configuration. + + Returns + ------- + None + """ + logger("Setting the InfluxDB api_lables...") + + try: + self.api_labels = self.client.labels_api() + + except Exception as e: + logger_err(f"Error setting the InfluxDB 'labels_api' with error: {str(e)}") + sys.exit(1) + + self.labels_check() + + ### + ### VALUES + ### + def value_update(self, field_name: str, field_value: int, tag_name=None, tag_value=None) -> None: + """ + Method to update the value of a field_name at the InfluxDb. + + Parameters + ---------- + field_name: str + The name of the field to be updated. + field_value: int + The value of the field to be updated. + + Returns + ------- + None + """ + logger(f"Updating value at the InfluxDB from '{str(field_name)}' with the value: {str(field_value)}") + + # Set Point + point = None + if tag_name and tag_value: + point = ( + Point(self.measurement) + .tag(tag_name, tag_value) + .field(field_name, field_value) + ) + else: + point = ( + Point(self.measurement) + .field(field_name, field_value) + ) + + try: + # Write to influx + write_api = self.client.write_api(write_options=SYNCHRONOUS) + write_api.write( + bucket=self.bucket, + org=self.org_name, + record=point + ) + + except Exception as e: + logger_err(f"Error updating the value for '{field_name}' bucket with error: {str(e)}") + + def value_get_last(self, field_name: str) -> int: + """ + Method to get the value of a field_name from the InfluxDb. + + Parameters + ---------- + field_name: str + The name of the field to get the last value. + + Returns + ------- + value_last: int + The last value from the the field. + """ + logger(f"Getting the value at the InfluxDB from: {str(field_name)}") + + value_last = None + query = f"""from(bucket:"{self.bucket}") + |> range(start:0) + |> filter(fn: (r) => + r._measurement == "{self.measurement}" and + r._field == "{field_name}" + ) + |> last()""" + + try: + last_data = self.client.query_api().query(query) + value_last = last_data[0].records[0]['_value'] + + except Exception as e: + logger_err(f"Error getting the last value from the bucket with error: {str(e)}") + + return value_last + + ### + ### ORGANIZATION + ### + def organization_check(self) -> str: + """ + Method to check if the default organization already exists. + + Returns + ------- + org_id: str + The organization ID. + """ + logger(f"Checkin if the organization with name: {self.org_name} already exixts...") + + founded_organizations = self.api_organizations.find_organizations() + + org_id = None + org_founded = None + for organization in founded_organizations: + if organization.name == self.org_name: + org_founded = organization + + if org_founded is not None: + org_id = org_founded.id + else: + org_id = self.organization_create() + + return org_id + + def organization_create(self) -> str: + """ + Method to create the default organization. + + Returns + ------- + org_id: str + The ID from the new created organization. + """ + logger(f"Creating the default organization with name: {self.org_name}") + + org_id = None + try: + new_org = self.api_organizations.create_organization(name=self.org_name) + org_id = new_org.id + logger(f"Created organization with name: '{new_org.name}'") + + except Exception as e: + logger_err(f"Error creating organization with error: {str(e)}") + sys.exit(1) + + return org_id + + ### + ### BUCKET + ### + def bucket_check(self) -> None: + """ + Method to check if the default bucket already exists. + + Returns + ------- + None + """ + logger(f"Checkin if the bucket with name: {self.bucket} already exixts...") + + ### Checks if the bucket exists + my_bucket = self.api_buckets.find_bucket_by_name(bucket_name=self.bucket) + + ### If it doesn't exists, create + if my_bucket is None: + self.bucket_create() + + else: + self.bucket_init_values_all() + + def bucket_create(self) -> None: + """ + Method to create the default bucket. + + Returns + ------- + None + """ + logger(f"Creating the default bucket with name: {self.bucket}") + + created_bucket = None + + ### Create bucket + try: + retention_rules = BucketRetentionRules( + type="expire", + shard_group_duration_seconds=3600, + every_seconds=self.bucket_retention_days + ) + + created_bucket = self.api_buckets.create_bucket( + bucket=None, + bucket_name=self.bucket, + org_id=None, + retention_rules=retention_rules, + description=self.bucket_description, + org=self.org_name + ) + + logger(f"Created bucket with name: '{created_bucket.name}'") + + except Exception as e: + logger_err(f"Error creating bucket with error: {str(e)}") + + ### Check if values ate empty + self.bucket_init_values_all() + + def bucket_init_values_all(self) -> None: + """ + Method to initializes all the value at the bucket if it's empty. + + Returns + ------- + None + """ + logger("Initializing all the values at the bucket if empty") + + self.bucket_init_values(field_name=self.field_name_uploads) + self.bucket_init_values(field_name=self.field_name_files) + self.bucket_init_values(field_name=self.field_name_prints) + self.bucket_init_values(field_name=self.field_name_pages) + self.bucket_init_values(field_name=self.field_name_size) + self.bucket_init_values(field_name=self.field_name_simultaneous_uploads) + + def bucket_init_values(self, field_name: str) -> None: + """ + Method to initializes a value at the bucket if it's empty. + + Parameters + ---------- + field_name: str + The name of the field to be initializaded. + + Returns + ------- + None + """ + logger(f"Initializing if not setted values for the field: {field_name}") + + query = f"""from(bucket:"{self.bucket}") + |> range(start:0) + |> filter(fn: (r) => + r._measurement == "{self.measurement}" and + r._field == "{field_name}" + ) + |> last()""" + + try: + last_data = self.client.query_api().query(query) + if len(last_data) == 0: + self.value_update(field_name=field_name, field_value=0) + + except Exception as e: + err_msg = f"Error getting/setting the first values from {field_name} at the bucket with error: {str(e)}" + logger_err(err_msg) + + ### + ### LABELS + ### + def labels_check(self) -> None: + """ + Method to check if the default labels already exists. + + Returns + ------- + None + """ + logger("Checkin if the default labels already exixts...") + + ### Get all labels + founded_labels = self.api_labels.find_labels() + + ### Check for label, if dont exist, create + label_names = [] + for label in founded_labels: + label_names.append(label.name) + + # for default_label in DEFAULT_BUCKET_LABELS: + for default_label in DEFAULT_VALUES.get('DEFAULT_INFLUXDB_BUCKET_LABELS'): + if default_label.get('name') not in label_names: + self.labels_create(default_label) + + def labels_create(self, default_label) -> None: + """ + Method to create InfluxDB bucket labels. + + Parameters + ---------- + default_label: str + The name of the label to be created. + + Returns + ------- + None + """ + ### Set vars + label_name = default_label.get('name') + properties = { + 'color': default_label.get('color'), + 'description': default_label.get('description') + } + + logger(f"Creating bucket label for: {label_name}") + + ### Create label + try: + new_label = self.api_labels.create_label( + name=label_name, + org_id=self.org_id, + properties=properties + ) + + logger(f"Created label: '{new_label.name}' with properties: '{new_label.properties}'") + + except Exception as e: + logger_err(f"Error creating label with name: {label_name} with error: {e}") diff --git a/src/utils/app_db.py b/src/utils/app_local_db.py similarity index 96% rename from src/utils/app_db.py rename to src/utils/app_local_db.py index 30238696948c5237c1fbd0f2b82291d693c8b50d..a9eb5d79ed50f8414e97e6e40deb95115f010a45 100644 --- a/src/utils/app_db.py +++ b/src/utils/app_local_db.py @@ -28,9 +28,9 @@ except ImportError as e: DEFAULT_NAME_DB = "db.json" -class AppDB(): +class AppLocalDB(): """ - Class for setting the files DB of the app. + Class for setting the local files DB of the app. """ def __init__(self, config) -> None: @@ -57,8 +57,6 @@ class AppDB(): logger("Setting the TinyDB from the config...") if self.config.use_mem_db: - # from tinydb.storages import JSONStorage - # from tinydb.middlewares import CachingMiddleware self.db = TinyDB(self.full_file_path_db, storage=CachingMiddleware(JSONStorage)) logger("DB setted for memory use only...") else: diff --git a/src/utils/app_printer.py b/src/utils/app_printer.py index 82ffc35110d24e8480cd5a0bd75acbd463db10f8..a27f2852bc66767ca58c09ba5eb92e3a83c7662c 100644 --- a/src/utils/app_printer.py +++ b/src/utils/app_printer.py @@ -36,7 +36,7 @@ class AppPrinter(): def __init__(self, config) -> None: """ - Function for initialiazing the class. + Function for initialiazing the AppPrinter class. Sets the class variables. Returns @@ -44,6 +44,7 @@ class AppPrinter(): None """ + self.config = config self.set_dict_printers_avail() self.printer_name = config.printer_name @@ -70,8 +71,13 @@ class AppPrinter(): """ logger(f"Printing from path: '{file_path}'") - conn = cups.Connection() - conn.printFile(self.printer_name, file_path, "", {}) + ### PRINT + if self.config.debug_disable_printing: + logger_warn("utils.AppPrinter.print -> printing disable") + + else: + conn = cups.Connection() + conn.printFile(self.printer_name, file_path, "", {}) def print_with_options( self, @@ -146,16 +152,15 @@ class AppPrinter(): logger_warn(err_msg) raise Exception(err_msg) - ### DEBUG - # from pprint import pp - # print("---> PRINTING") - # pp(print_options) - ### PRINT - conn = cups.Connection() - conn.printFile( - printer=self.printer_name, - filename=file_path, - title="This_have_to_be_setted_with_something", - options=print_options - ) + if self.config.debug_disable_printing: + logger_warn("utils.AppPrinter.print_with_options -> printing disable") + + else: + conn = cups.Connection() + conn.printFile( + printer=self.printer_name, + filename=file_path, + title="This_have_to_be_setted_with_something", + options=print_options + ) diff --git a/src/utils/app_usage.py b/src/utils/app_usage.py index 1ad0ca57a89b940099d15afff8fdd33db2efc717..6cef5c1a0b0a94610dd7f9d5cf4bdd4d43478610 100644 --- a/src/utils/app_usage.py +++ b/src/utils/app_usage.py @@ -8,16 +8,16 @@ ### ### DEPENDENCIES ### +# import os +# import os.path import sys -import os -import os.path import configparser ### LOCAL try: from utils.app_logger import logger except ImportError as e: - print("\n\n Unable to import 'utils.app_logger.py' from the 'web.routes.py' file from project \n\n") + print("\n\n Unable to import 'utils.app_logger.py' from the 'utils.app_usage.py' file from project \n\n") sys.exit(1) @@ -32,7 +32,7 @@ class AppUsage(): Class for setting the usage of the app. """ - def __init__(self, config) -> None: + def __init__(self, config, influxdb=None) -> None: """ Method for initialiazing the AppUsage class. Sets the class variables. @@ -50,7 +50,80 @@ class AppUsage(): self.full_file_path_usage = config.path_to_data + '/' + DEFAULT_USAGE_FILE self.usage = configparser.ConfigParser() - def get_total_files_in_folder(self) -> int: + self.influxdb = influxdb + + def update_from_upload(self, file_info) -> None: + """ + Method to update the usage from an upload. + + Parameters: + file_info: dict + The dictionary with the file information. + + Returns + ------- + None + """ + logger("Updating the usage values from an upload...") + + ### Update Uploads + self.counter_uploads_update() + ### Update Files + self.counter_files_update(update_type='upload') + ### Max simultaneous uploads + self.counter_max_simultaneous_uploaded_update() + ### Update Size + self.counter_size_update(file_info=file_info) + + def update_from_delete(self) -> None: + """ + Method to update the usage from a deletion. + + Returns + ------- + None + """ + logger("Updating the usage values from an deletion...") + + ### Update Files + self.counter_files_update(update_type='delete') + + def update_from_print(self, file_info=None) -> None: + """ + Method to update the usage from a printed file. + + Returns + ------- + None + """ + logger("Updating the usage values from a printed file...") + + self.counter_prints_update() + self.counter_pages_update(file_info) + + def remove_all_files(self) -> None: + """ + Method to remove usage from all the files on the system at the moment. + + Returns + ------- + None + """ + logger("Updating the usage values from the deletion from all files...") + + ### Set the values of the files as 0 + if self.influxdb is None: + self.usage.set('FILES', 'total', str(0)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) + + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_files, field_value=0) + + #### + #### OLLLLLLLD + #### + def OLD_get_total_files_in_folder(self) -> int: """ Method to get the total files at the upload folder. @@ -59,10 +132,24 @@ class AppUsage(): int: The total files at the upload folder. """ - logger("Getting total files on the system...") + logger("Getting total files at the upload folder on the system...") + + # tot_files = 0 + # if self.influxdb is None: + # # tot_files = len([name for name in os.listdir(self.config.path_to_upload) if os.path.isfile(os.path.join(self.config.path_to_upload, name))]) + # tot_files = self.counter_files_get() + + # else: + # tot_files = self.influxdb_counter_files_get() + + # return tot_files + return self.counter_files_get() - return len([name for name in os.listdir(self.config.path_to_upload) if os.path.isfile(os.path.join(self.config.path_to_upload, name))]) + ### + ### COUNTER + ### + ### UPLOADS def counter_uploads_get(self) -> int: """ Method to get the total uploads from the system. @@ -72,10 +159,17 @@ class AppUsage(): int: The total uploads. """ - logger("Fetched the counter for total uploaded files on Disk...") + logger("Fetched the counter for total uploads...") - self.usage.read(self.full_file_path_usage) - return int(self.usage.get('UPLOADS', 'total')) + tot_uploads = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_uploads = int(self.usage.get('UPLOADS', 'total')) + + else: + tot_uploads = self.influxdb.value_get_last(self.influxdb.field_name_uploads) + + return tot_uploads def counter_uploads_update(self) -> None: """ @@ -85,15 +179,67 @@ class AppUsage(): ------- None """ - logger("Get the total uploaded files on Disk...") + logger("Update the counter for total uploads...") tot_uploads = self.counter_uploads_get() + 1 - self.usage.set('UPLOADS', 'total', str(tot_uploads)) + if self.influxdb is None: + self.usage.set('UPLOADS', 'total', str(tot_uploads)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) - with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: - self.usage.write(file) + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_uploads, field_value=tot_uploads) + ### FILES + def counter_files_get(self) -> int: + """ + Method to get the total files from the system. + + Returns + ------- + int: + The total uploads. + """ + logger("Fetched the counter for total files...") + + tot_files = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_files = int(self.usage.get('FILES', 'total')) + + else: + tot_files = self.influxdb.value_get_last(self.influxdb.field_name_files) + + return tot_files + + def counter_files_update(self, update_type: str) -> None: + """ + Method to update the counter for total files from the system. + + Returns + ------- + None + """ + logger("Update the counter for total files...") + + files_at_the_moment = self.counter_files_get() + + tot_files = None + if update_type == 'upload': + tot_files = files_at_the_moment + 1 + elif update_type == 'delete': + tot_files = files_at_the_moment - 1 + + if self.influxdb is None: + self.usage.set('FILES', 'total', str(tot_files)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) + + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_files, field_value=tot_files) + + ### PRINTS def counter_prints_get(self) -> int: """ Method to get the total prints from the system. @@ -105,8 +251,15 @@ class AppUsage(): """ logger("Fetched the counter for total prints...") - self.usage.read(self.full_file_path_usage) - return int(self.usage.get('PRINTS', 'total')) + tot_prints = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_prints = int(self.usage.get('PRINTS', 'total')) + + else: + tot_prints = self.influxdb.value_get_last(self.influxdb.field_name_prints) + + return tot_prints def counter_prints_update(self) -> None: """ @@ -116,15 +269,19 @@ class AppUsage(): ------- None """ - logger("Updated the counter for total Prints...") + logger("Updated the counter for total prints...") tot_prints = self.counter_prints_get() + 1 - self.usage.set('PRINTS', 'total', str(tot_prints)) + if self.influxdb is None: + self.usage.set('PRINTS', 'total', str(tot_prints)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) - with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: - self.usage.write(file) + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_prints, field_value=tot_prints) + ### PAGES def counter_pages_get(self) -> int: """ Method to get the total pages printed from the system. @@ -134,10 +291,17 @@ class AppUsage(): int: The total pages printed. """ - logger("Fetched the counter for total pages printed...") + logger("Fetched the counter for total pages...") + + tot_pages = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_pages = int(self.usage.get('PAGES', 'total')) - self.usage.read(self.full_file_path_usage) - return int(self.usage.get('PAGES', 'total')) + else: + tot_pages = self.influxdb.value_get_last(self.influxdb.field_name_pages) + + return tot_pages def counter_pages_update(self, file_info) -> None: """ @@ -147,15 +311,19 @@ class AppUsage(): ------- None """ - logger("Updated the counter for total pages printed...") + logger("Updated the counter for total pages...") tot_pages = self.counter_pages_get() + file_info.get('num_pages') - self.usage.set('PAGES', 'total', str(tot_pages)) + if self.influxdb is None: + self.usage.set('PAGES', 'total', str(tot_pages)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) - with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: - self.usage.write(file) + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_pages, field_value=tot_pages) + ### MAX SIMULTANEOUS UPLOADS def counter_max_simultaneous_uploaded_get(self) -> int: """ Method to get the maximal simultaneous uploads from the system. @@ -165,10 +333,17 @@ class AppUsage(): int: The maximal simultaneous uploads. """ - logger("Fetched the counter for total pages printed...") + logger("Fetched the counter for maximal simultaneous uploads...") - self.usage.read(self.full_file_path_usage) - return int(self.usage.get('MAX_SIMULTANEOUS_UPLOADS', 'total')) + tot_simult_uploads = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_simult_uploads = int(self.usage.get('MAX_SIMULTANEOUS_UPLOADS', 'total')) + + else: + tot_simult_uploads = self.influxdb.value_get_last(self.influxdb.field_name_simultaneous_uploads) + + return tot_simult_uploads def counter_max_simultaneous_uploaded_update(self) -> None: """ @@ -178,16 +353,116 @@ class AppUsage(): ------- None """ - logger("Updated the counter for total pages printed...") + logger("Updated the counter for maximal simultaneous uploads...") # Get the total tot_simult = self.counter_max_simultaneous_uploaded_get() # Get the total uploaded files at the folder right now - tot_files_in_folder = self.get_total_files_in_folder() + tot_files_in_folder = self.counter_files_get() if tot_simult < tot_files_in_folder: - self.usage.set('MAX_SIMULTANEOUS_UPLOADS', 'total', str(tot_files_in_folder)) + if self.influxdb is None: + self.usage.set('MAX_SIMULTANEOUS_UPLOADS', 'total', str(tot_files_in_folder)) + with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: + self.usage.write(file) + + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_simultaneous_uploads, field_value=tot_files_in_folder) + + ### SIZE + def counter_size_get(self) -> int: + """ + Method to get the total size uploaded from the system. + + Returns + ------- + int: + The total size uploaded. + """ + logger("Fetched the counter for total files uploaded size...") + + tot_size = None + if self.influxdb is None: + self.usage.read(self.full_file_path_usage) + tot_size = int(self.usage.get('TOTAL_SIZE_UPLOAD', 'total')) + + else: + tot_size = self.influxdb.value_get_last(self.influxdb.field_name_size) + + return tot_size + + def counter_size_update(self, file_info) -> None: + """ + Method to update the counter for the total uploaded size from the system. + + Returns + ------- + None + """ + logger("Updated the counter for total size uploaded...") + + tot_size = self.counter_size_get() + file_info.get('size') + + if self.influxdb is None: + self.usage.set('TOTAL_SIZE_UPLOAD', 'total', str(tot_size)) with open(self.full_file_path_usage, 'w', encoding="utf-8") as file: self.usage.write(file) + + else: + self.influxdb.value_update(field_name=self.influxdb.field_name_size, field_value=tot_size) + + ### + ### INFLUXDB + ### + def influxdb_values_rotate(self): + """ + Method to update the counter for total uploads at the InfluxDb. + + Returns + ------- + None + """ + logger("Rotate the values on the InfluxDB...") + + ### UPLOADS + tot_uploads = self.counter_uploads_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_uploads, field_value=tot_uploads) + + ### MAX SIMULTANEOUS UPLOADS + max_simultaneous_uploaded = self.counter_max_simultaneous_uploaded_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_simultaneous_uploads, field_value=max_simultaneous_uploaded) + + ### FILES + tot_files = self.counter_files_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_files, field_value=tot_files) + + ### PRINTS + tot_prints = self.counter_prints_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_prints, field_value=tot_prints) + + ### PAGES + tot_pages = self.counter_pages_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_pages, field_value=tot_pages) + + ### SIZE + tot_size = self.counter_size_get() + self.influxdb.value_update(field_name=self.influxdb.field_name_size, field_value=tot_size) + + def influxdb_values_reset(self): + """ + Method to reset all values at the InfluxDb. + + Returns + ------- + None + """ + logger("Reseting all the values on the InfluxDB...") + + self.influxdb.value_update(field_name=self.influxdb.field_name_uploads, field_value=0) + self.influxdb.value_update(field_name=self.influxdb.field_name_files, field_value=0) + self.influxdb.value_update(field_name=self.influxdb.field_name_prints, field_value=0) + self.influxdb.value_update(field_name=self.influxdb.field_name_pages, field_value=0) + self.influxdb.value_update(field_name=self.influxdb.field_name_size, field_value=0) + self.influxdb.value_update(field_name=self.influxdb.field_name_simultaneous_uploads, field_value=0) diff --git a/src/web/__init__.py b/src/web/__init__.py index f56ecd5446ed185ec735947c4cc69d079c8edfec..1800af2609998c72e29f81295c2125f669012696 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -9,6 +9,7 @@ from web.routes_user import AppRoutesUser from web.routes_protected import AppRoutesProtected from web.routes_debug import AppRoutesDebug from web.routes_question import AppRoutesQuestion +from web.routes_api import AppRoutesApi __author__ = "fejao, rot13" diff --git a/src/web/routes_api.py b/src/web/routes_api.py new file mode 100644 index 0000000000000000000000000000000000000000..adf9a9d2742f7eaf66e90ff5d896f93c4e60ccc4 --- /dev/null +++ b/src/web/routes_api.py @@ -0,0 +1,752 @@ +#!/usr/bin/env python3 +""" + web.routes_api.py file for the c3 printing system. + In this file it will be set the routes for accessing the server trought API calls. +""" + +### DEBUG +# import json +import random + +### +### DEPENDENCIES +### +import sys +# from datetime import datetime, timedelta +from datetime import timedelta +from flask import request +# import flask_login +from flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, verify_jwt_in_request + +### LOCAL +try: + from utils.app_logger import logger, logger_err +except ImportError as e: + print("\n\n Unable to import 'utils.app_logger.py' from the 'web.routes_api.py' file from project \n\n") + sys.exit(1) + +try: + from web.routes_default_vals import DEFAULT_ROUTE_VALS +except ImportError as e: + print("\n\n Unable to import 'web.routes_defaul_vals.py' from the 'web.routes_api.py' file from project \n\n") + sys.exit(1) + + +class AppRoutesApi(): + """ + Class for the API routes of the Flask Server. + """ + + def __init__(self, config, jwt, users, usage, printer) -> None: + """ + Function for initialiazing the AppRoutesApi class. + Sets the class variables. + + Parameters + ---------- + config: utils.AppConfiguration + The config class object. + jwt: JWTManager + The JWTManager object. + users: + foo + usage: utils.AppUsage + The usage class object. + printer: utils.AppPrinter + The printer class object. + + Returns + ------- + None + """ + self.config = config + self.jwt = jwt + self.users = users + self.usage = usage + self.printer = printer + self.default_layout_admin = DEFAULT_ROUTE_VALS.get('LAYOUT_ADMIN') + # Init + self.token_access = None + self.token_refresh = None + + def set_init_tokens(self) -> None: + """ + Method for initializing the API tokens. + + Returns + ------- + None + """ + logger("Creating API Access Token...") + + email = self.config.login_api_email + + self.token_access = create_access_token( + identity=email, + expires_delta=timedelta(hours=self.config.api_token_expire_hours) + ) + + self.token_refresh = create_refresh_token( + identity=email, + expires_delta=timedelta(hours=self.config.api_token_expire_hours) + ) + + def _create_access_token(self, email) -> str: + """ + Method for creating an access token for the API. + + Returns + ------- + str: + The created access token. + """ + logger("Creating API Access Token...") + + self.token_access = create_access_token( + identity=email, + expires_delta=timedelta(hours=self.config.api_token_expire_hours) + ) + + return self.token_access + + def _create_refresh_token(self, email) -> str: + """ + Method for creating an refresh token for the API. + + Returns + ------- + str: + The created refresh token. + """ + logger("Creating API Refresh Token...") + + self.token_refresh = create_refresh_token( + identity=email, + expires_delta=timedelta(hours=self.config.api_token_expire_hours) + ) + + return self.token_refresh + + def _fetch_usage(self) -> dict: + """ + Method for getting the usage at the moment from the App. + + Returns + ------- + dict: + The dictionary with the info from the App usage. + """ + logger("Creating API Access Token...") + + return { + 'tot_uploads': self.usage.counter_uploads_get(), + 'tot_prints': self.usage.counter_prints_get(), + 'tot_pages': self.usage.counter_pages_get(), + 'tot_files': self.usage.counter_files_get(), + 'tot_size': self.usage.counter_size_get(), + 'tot_simultaneous_uploads': self.usage.counter_max_simultaneous_uploaded_get() + } + + ### /api/create-login-token + def create_login_token(self): + """ + Endpoint for render the '/api/create-login-token' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from creating a login token. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/create-login-token'") + + ### VARS + ret_dict = { + 'status_ok': False, + 'msg': 'init from ret_dict' + } + ret_code = 404 + + ### ONLY NEW TOKENS TO BE SETTED BY LOGIN + if not self.config.debug: + logger_err("Trying to create token in not a debug mode") + ret_dict = { + 'status_ok': False, + 'msg': 'Setting new tokens are disable...' + } + ret_code = 406 + + return ret_dict, ret_code + + ### GET PARAMS + email = request.authorization.parameters.get('username') + password = request.authorization.parameters.get('password') + + ### CHECK + if not email: + ret_dict = { + "status_ok": False, + "error_msg": "Request parameter not set: username" + } + ret_code = 401 + + elif not password: + ret_dict = { + "status_ok": False, + "error_msg": "Request parameter not set: password" + } + ret_code = 401 + + else: + if email != self.config.login_api_email: + ret_dict = { + "status_ok": False, + "error_msg": "Bad username" + } + ret_code = 401 + + elif password != self.config.login_api_passwd: + ret_dict = { + "status_ok": False, + "error_msg": "Bad password" + } + ret_code = 401 + + else: + ret_dict = { + 'status_ok': True, + 'message': 'Token created', + 'access_token': self._create_access_token(email), + 'expires_in_hours': self.config.api_token_expire_hours + } + ret_code = 200 + + return ret_dict, ret_code + + ### /api/create-refresh-token + def create_refresh_token(self): + """ + Endpoint for render the '/api/create-refresh-token' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from creating a refresh token. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/create-refresh-token'") + + ### VARS + ret_dict = { + 'status_ok': False, + 'msg': 'init from ret_dict' + } + ret_code = 404 + + ### Check header + try: + verify_jwt_in_request() + + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Error accessing with message: '{e}'" + } + ret_code = 401 + + return ret_dict, ret_code + + try: + current_user = get_jwt_identity() + # refresh_token = self._create_refresh_token(email=current_user) + + ret_dict = { + 'status_ok': True, + 'message': 'Refresh token created', + # 'refresh_token': refresh_token, + 'refresh_token': self._create_refresh_token(email=current_user), + 'expires_in_hours': self.config.api_token_expire_hours + } + ret_code = 200 + except Exception as e: + # return jsonify({"msg": "Failed to refresh token"}), 400 + ret_dict = { + 'status_ok': False, + 'error_message': f"Error refreshing token with message: '{e}'" + } + ret_code = 400 + + return ret_dict, ret_code + + ### /api/refresh-token + def refresh_created_token(self): + """ + Endpoint for render the '/api/refresh-token' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from refreshing a token. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/refresh-token'") + + ### VARS + ret_dict = { + 'status_ok': False, + 'msg': 'init from ret_dict' + } + ret_code = 404 + + ### Check header + try: + # verify_jwt_in_request() + verify_jwt_in_request(refresh=True) + + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Error accessing with message: '{e}'" + } + ret_code = 401 + + return ret_dict, ret_code + + # refresh token + try: + current_user = get_jwt_identity() + # access_token = self._create_access_token(email=current_user) + + ret_dict = { + 'status_ok': True, + 'message': 'Token refreshed', + # 'access_token': access_token, + 'access_token': self._create_access_token(email=current_user), + 'expires_in_hours': self.config.api_token_expire_hours + } + ret_code = 200 + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Error refreshing token with message: '{e}'" + } + ret_code = 400 + + return ret_dict, ret_code + + ### /api/get-usage + def get_usage(self): + """ + Endpoint for render the '/api/get-usage' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from the App usage at the moment. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/get-usage'") + + ### VARS + ret_dict = { + 'status_ok': False, + 'msg': 'init from ret_dict' + } + ret_code = 404 + + ### Check header + try: + verify_jwt_in_request() + + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Error accessing with message: '{e}'" + } + ret_code = 401 + + return ret_dict, ret_code + + ret_dict = { + 'status_ok': True, + 'usage': self._fetch_usage() + } + ret_code = 200 + + return ret_dict, ret_code + + ### /api/test + def test(self): + """ + Endpoint for render the '/api/test' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from testing the API. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/test'") + + ### VARS + ret_dict = { + 'status_ok': False, + 'msg': 'init from ret_dict' + } + ret_code = 404 + + ### Check header + try: + verify_jwt_in_request() + + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Error accessing with message: '{e}'" + } + ret_code = 401 + + return ret_dict, ret_code + + ### Check user + try: + current_user = get_jwt_identity() + + ret_dict = { + 'status_ok': True, + 'message': f'Hello, {current_user}' + } + ret_code = 200 + + except Exception as e: + ret_dict = { + 'status_ok': False, + 'error_message': f"Login parameters not founded at the request, with error: '{e}'" + } + ret_code = 401 + + return ret_dict, ret_code + + ### + ### GRAFANA + ### + + # https://www.youtube.com/watch?v=lgZMRTST444 + # https://nagasudhir.blogspot.com/2024/05/json-grafana-plugin-to-fetch-api-data.html + # https://grafana.com/grafana/plugins/simpod-json-datasource/ + + # - '/' + # - '/metrics' + # - '/metric-payload-options' + # - '/query' + + ### /api/ + def grafana_home(self): + """ + Endpoint for render the '/api' end-point. + + Returns + ------- + str: + Empty string to be recognized from Graphana. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api'") + + return "", 200 + + ### /api/metrics + def grafana_metrics(self): + """ + Endpoint for render the '/api/metrics' end-point. + + Returns + ------- + ret_dict: dict + The dictionary with the answer from the App metrics at the moment. + ret_code: int + The number set for returning the response. + """ + logger("Open route for '/api/metrics'") + + ret_dict = [ + { + # Optional. If the value is empty, use the value as the label + "label": "Describe metric list", + # The value of the option. + "value": "DescribeMetricList", + # Configuration parameters of the payload. + "payloads": [ + { + # The label of the payload. If the value is empty, use the name as the label. + "label": "Namespace", + # The name of the payload. + "name": "namespace", + # If the value is select, the UI of the payload is a radio box. + # If the value is multi-select, the UI of the payload is a multi selection box; + # if the value is input, the UI of the payload is an input box; + # if the value is textarea, the UI of the payload is a multiline input box. The default is input. + "type": "select", + # Input box / selection box prompt information. + "placeholder": "Please select namespace", + # Whether to overload the metrics API after modifying the value of the payload. + "reloadMetric": True, + # Set the input / selection box width to a multiple of 8px. + "width": 10, + # If the payload type is select / multi-select, the list is the configuration of the option list. + "options": [ + { + # The label of the payload select option. + "label": "acs_mongodb", + # The label of the payload value. + "value": "acs_mongodb", + }, { + "label": "acs_rds", + "value": "acs_rds", + } + ] + }, { + "name": "metric", + "type": "select" + }, { + "name": "instanceId", + "type": "select" + } + ] + }, { + "label": "c3printer Totals", + "value": "c3printer_totals", + # Configuration parameters of the payload. + "payloads": [ + { + # The label of the payload. If the value is empty, use the name as the label. + "label": "Total ", + # The name of the payload. + "name": "usage", + # If the value is select, the UI of the payload is a radio box. + # If the value is multi-select, the UI of the payload is a multi selection box; + # if the value is input, the UI of the payload is an input box; + # if the value is textarea, the UI of the payload is a multiline input box. The default is input. + "type": "select", + # Input box / selection box prompt information. + "placeholder": "Please select usage", + # Whether to overload the metrics API after modifying the value of the payload. + "reloadMetric": True, + # Set the input / selection box width to a multiple of 8px. + "width": 10, + # If the payload type is select / multi-select, the list is the configuration of the option list. + "options": [ + { + "label": "Total Files in Folder", + "value": "tot_files_in_folder", + }, { + "label": "Total Files Uploaded", + "value": "tot_uploads" + }, { + "label": "Total Pages Printed", + "value": "tot_pages" + }, { + "label": "Maximal Simultaneous Uploads", + "value": "max_simultaneous_uploads" + }, + ] + } + ] + }, + { + "label": "Describe Metric Last", + "value": "describe_metric_last", + "payloads": [ + { + "name": "namespace", + "type": "select" + }, { + "name": "metric", + "type": "select" + }, { + "name": "instanceId", + "type": "multi-select" + } + ] + } + ] + + return ret_dict, 200 + + ### /api/metric-payload-options + def grafana_metric_payload_options(self): + """ + Endpoint for render the '/api/metric-payload-options' page. + + Returns + ------- + wrappers.Response: + The JSON and CODE from logging at the API. + """ + logger("Open route for '/api/metric-payload-options'") + + ret_dict = [ + { + "label": "Total Files in Folder", + "value": "tot_files_in_folder" + }, { + "label": "Total Files Uploaded", + "value": "tot_uploads" + }, { + "label": "Total Pages Printed", + "value": "tot_pages" + }, { + "label": "Maximal Simultaneous Uploads", + "value": "max_simultaneous_uploads" + } + ] + + return ret_dict, 200 + + ### /api/query + def grafana_query(self): + """ + Endpoint for render the '/api/query' page. + + Returns + ------- + wrappers.Response: + The JSON and CODE from logging at the API. + """ + logger("Open route for '/api/query'") + + # { + # 'app': 'dashboard', + # 'dashboardUID': 'dea6gv30fgoowf', + # 'interval': '1m', + # 'intervalMs': 60000, + # 'maxDataPoints': 925, + # 'panelId': 1, + # 'panelPluginId': 'table', + # 'range': {'from': '2025-01-16T05:42:11.941Z', + # 'raw': {'from': 'now-12h', 'to': 'now'}, + # 'to': '2025-01-16T17:42:11.941Z'}, + # 'rangeRaw': {'from': 'now-12h', 'to': 'now'}, + # 'requestId': 'SQR113', + # 'scopedVars': {'__interval': {'text': '1m', 'value': '1m'}, + # '__interval_ms': {'text': '60000', 'value': 60000}, + # '__sceneObject': {'text': '__sceneObject'}}, + # 'scopes': [], + # 'startTime': 1737049331949, + # 'targets': [{'datasource': {'type': 'simpod-json-datasource', + # 'uid': 'fea6fzbhavshsa'}, + # 'editorMode': 'code', + # 'payload': {'usage': 'tot_files_in_folder'}, + # 'refId': 'test1', + # 'target': 'c3printer_totals'}], + # 'timezone': 'browser' + # } + + ### VARS + # data_dict = None + # payload_usage = None + # ref_id = None + + target_target = None + target_payload = None + target_data = None + + ret_payload = None + + ### GET payload_usage + # usage = dict(request.data).get('usage') + + ### GET QUERY DATA + query_data = request.get_json() + # ### TIME + # time_start = datetime.strptime(query_data["range"]["from"], "%Y-%m-%dT%H:%M:%S.%fZ") + # time_end = datetime.strptime(query_data["range"]["to"], "%Y-%m-%dT%H:%M:%S.%fZ") + + response = [] + for target in query_data.get('targets'): + target_target = target.get('target') + target_payload = target.get('payload', {}) + target_data = { + 'target': target.get('refId'), + 'datapoints': [] + } + + if target_target == 'c3printer_totals': + if target_payload == 'tot_files_in_folder': + ret_payload = self.usage.counter_files_get() + + # else: + # import pdb; pdb.set_trace() + print(ret_payload) + + counter = 0 + while counter < 5: + counter += 1 + target_data['datapoints'].append( + [random.randint(100, 300), int(10 * 1000)] + ) + + response.append(target_data) + + # samplFreq = int(target_payload.get("sampling_freq", "60")) + + # data_dict = json.loads(request.data) + # for target in data_dict.get('targets'): + # if target.get('target') == 'c3printer_totals': + # payload_usage = target.get('payload').get('usage') + # ref_id = target.get('refId') + + # if payload_usage == 'tot_files_in_folder': + # import pdb; pdb.set_trace() + + # else: + # import pdb; pdb.set_trace() + # print("----> 1") + + # ### IF REQ GET + # if request.method == 'GET': + # print("wtf") + + # # from datetime import datetime + # dt_1 = datetime(2025, 1, 16, 15, 0) + # dt_2 = datetime(2025, 1, 16, 16, 0) + # dt_3 = datetime(2025, 1, 16, 17, 0) + + # # Convert to epoch time + # epoch_time = dt.timestamp() + # print(epoch_time) + + # ret_query = { + # 'target': 'test1', + # 'dataponts': [ + # [1, dt_1.timestamp()], + # [2, dt_2.timestamp()], + # [3, dt_3.timestamp()] + # ] + # } + + ############ + ############ + ############ + + # ret_query = [ + # { + # 'target': 'test1', + # 'dataponts': [ + # [1, dt_1.timestamp()], + # [2, dt_2.timestamp()], + # [3, dt_3.timestamp()] + # ] + # } + # ] + + # return ret_query, 200 + + ######### + return response, 200 diff --git a/src/web/routes_debug.py b/src/web/routes_debug.py index f6c1b4ff6ef019b5881cf44bd9c7c9273f112bc5..5e95b4fe6245e0505fa239075635f9b83b729ddf 100644 --- a/src/web/routes_debug.py +++ b/src/web/routes_debug.py @@ -30,7 +30,7 @@ class AppRoutesDebug(): Class for the user routes of the Flask Server. """ - def __init__(self, config, users, printer) -> None: + def __init__(self, config, users, printer, api=None) -> None: """ Function for initialiazing the AppRoutesDebug class. Sets the class variables. @@ -51,6 +51,7 @@ class AppRoutesDebug(): self.config = config self.users = users self.printer = printer + self.api = api self.default_layout_admin = DEFAULT_ROUTE_VALS.get('LAYOUT_ADMIN') @flask_login.login_required @@ -112,7 +113,7 @@ class AppRoutesDebug(): return redirect(url_for('home-admin')) @flask_login.login_required - def debug_admin(self): + def debug_sys(self): """ Endpoint for render the '/debug-admin' page. @@ -128,14 +129,67 @@ class AppRoutesDebug(): return "You do not have access to this page.", 403 ### GET PAGE STYLE AND VARS - default_vals = DEFAULT_ROUTE_VALS.get('DEBUG_ADMIN') + default_vals = DEFAULT_ROUTE_VALS.get('DEBUG_SYS') ### IF REQ GET if request.method == 'GET': ### RENDER return render_template( - "debug_admin.html", + "debug_sys.html", + enable_nav_link="debug-sys", + page_title=default_vals.get("page_title"), + page_texts=default_vals, + admin_layout=self.default_layout_admin, + debug=self.config.debug, + enabled_questions=self.config.enabled_questions, + enabled_about=self.config.enabled_about, + user=flask_login.current_user.id, + is_admin=flask_login.current_user.is_admin, + sys_config=self.config.get_dict_values() + ) + + @flask_login.login_required + def debug_api(self): + """ + Endpoint for render the '/debug-api' page. + + Returns + ------- + wrappers.Response: + The webpage for debugging the API. + """ + logger("Open route for '/debug-api'") + + # Check if the current user is an admin + if not flask_login.current_user.is_admin: + return "You do not have access to this page.", 403 + + ### GET PAGE STYLE AND VARS + default_vals = DEFAULT_ROUTE_VALS.get('DEBUG_API') + token_access = None + token_refresh = None + + ### GET API PARAMS + if self.api.token_access is None: + self.api.set_init_tokens() + + if self.api.token_access: + token_access = self.api.token_access + else: + token_access = 'No access token created for the API' + + if self.api.token_refresh: + token_refresh = self.api.token_refresh + else: + token_refresh = 'No refresh token created for the API' + + ### IF REQ GET + if request.method == 'GET': + + ### RENDER + return render_template( + "debug_api.html", enable_nav_link="debug-admin", page_title=default_vals.get("page_title"), page_texts=default_vals, @@ -145,5 +199,5 @@ class AppRoutesDebug(): enabled_about=self.config.enabled_about, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin, - sys_config=self.config.get_dict_values(), + api_config={'token_access': token_access, 'token_refresh': token_refresh} ) diff --git a/src/web/routes_default_vals.py b/src/web/routes_default_vals.py index 77a58bff068f0e5587d3bec2a47ba17fe7ccd607..d625fcc82f0696aa16172103dab9a0ec627e983e 100644 --- a/src/web/routes_default_vals.py +++ b/src/web/routes_default_vals.py @@ -24,7 +24,8 @@ DEFAULT_ROUTE_VALS['LAYOUT_ADMIN'] = { 'nav_bar_name_clean': 'Clean', 'nav_bar_name_questions': 'Most Asked Questions', 'nav_bar_name_debug_printers': 'Printer Debug', - 'nav_bar_name_debug_admin': 'System Debug', + 'nav_bar_name_debug_system': 'System Debug', + 'nav_bar_name_debug_api': 'API Debug', 'nav_bar_name_documentation': 'Documentation', 'nav_bar_name_logout': 'Logout', 'nav_bar_logo': DEFAULT_LOGO_SMALL, @@ -91,6 +92,7 @@ DEFAULT_ROUTE_VALS['HOME_ADMIN'] = { 'text_total_prints': 'Total Prints', 'text_total_pages': 'Total Pages Printed', 'text_total_files': 'Total Files at the Moment', + 'text_total_size': 'Total Uploaded Size Mb', 'text_max_simultaneous_uploaded': 'Max Simultaneous Uploads', 'text_message': 'Message', 'button_delete': 'Delete', @@ -102,23 +104,31 @@ DEFAULT_ROUTE_VALS['HOME_ADMIN'] = { ### /uploaded-list DEFAULT_ROUTE_VALS['UPLOADED_LIST'] = { 'page_title': "39c3 InfoDesk Printer Uploaded files list", - 'page_h1': "List of uploaded files", - 'text_total_files': 'Total files in the system at the moment', + 'page_h1': "List of uploaded files.", + 'page_h3': "The files onder then '{}' minutes will be deleted autoMAGICALLY.", + 'text_total_files': 'Total files in the system at the moment:', + 'text_total_folder_size': 'Total folder size in Mb at the moment:', 'text_file_code': 'File Code', 'text_file_preview': 'Preview', 'text_file_date_upload': 'Date Upload', 'text_file_original_name': 'Original Name', + 'text_file_size': 'File Size Mb', + 'text_file_pages': 'File Pages', 'button_preview': 'Preview' } ### /uploaded-clean DEFAULT_ROUTE_VALS['UPLOADED_CLEAN'] = { 'page_title': " 39c3 InfoDesk Printer Uploaded Files Clean", - 'page_h1': "List of uploaded files to be deleted", + 'page_h1': "List of uploaded files about to expire.", + 'page_h3': "The files onder then '{}' minutes will be deleted autoMAGICALLY.", + 'text_file_files_expired': 'Total Files expired:', 'text_file_code': 'File Code', - 'text_file_preview': 'Preview', + 'text_file_delete': 'Delete', 'text_file_date_upload': 'Date Upload', 'text_file_original_name': 'Original Name', + 'text_file_size': 'File Size Mb', + 'text_file_pages': 'File Pages', 'button_delete_all': 'Delete All', 'button_nuke_all': 'Nuke All Files', 'button_delete': 'Delete', @@ -184,12 +194,18 @@ DEFAULT_ROUTE_VALS['DEBUG_PRINTERS'] = { 'ret_msg_ok': "<br>Test print executed. <br>Test file at: '<b>{}</b>' should be printed at the printer '<b>{}</b>'", } -### /debug-printers -DEFAULT_ROUTE_VALS['DEBUG_ADMIN'] = { - 'page_title': " 39c3 InfoDesk Debug Admin", +### /debug-sys +DEFAULT_ROUTE_VALS['DEBUG_SYS'] = { + 'page_title': " 39c3 InfoDesk Debug Sys", 'page_h1': "Debugging the values setted on the system", } +### /debug-sys +DEFAULT_ROUTE_VALS['DEBUG_API'] = { + 'page_title': " 39c3 InfoDesk Debug API", + 'page_h1': "Debugging the values setted on the API", +} + ### ### ROUTES-QUESTIONS ### diff --git a/src/web/routes_protected.py b/src/web/routes_protected.py index e9e425e011453d16db66ede55e6669062d94a69d..e934142c9810a6b162430fafd79085889a1e55cf 100644 --- a/src/web/routes_protected.py +++ b/src/web/routes_protected.py @@ -27,6 +27,12 @@ except ImportError as e: print("\n\n Unable to import 'web.routes_defaul_vals.py' from the 'web.routes_protected.py' file from project \n\n") sys.exit(1) +try: + from utils.app_default_vals import DEFAULT_VALUES +except ImportError as e: + print("\n\n Unable to import 'utils.app_default_vals.py' from the 'web.routes_protected.py' file from project \n\n") + sys.exit(1) + ### ### HELPER @@ -108,7 +114,114 @@ class AppRoutesProtected(): self.users = users self.usage = usage self.default_layout_admin = DEFAULT_ROUTE_VALS.get('LAYOUT_ADMIN') + self.default_values = DEFAULT_VALUES + + def _uploaded_clean_delete(self, file) -> None: + """ + Method to call all methods for deleting a file. + + Returns + ------- + None + """ + logger(f"Cleaning/Deleting file with code: {file.get('file_code')}") + + ### DELETE FROM DB + self.db_files.file_delete(file_info=file) + ### DELETE FROM DISK + self.files.file_delete(file_info=file) + ### UPDATE USAGE + self.usage.update_from_delete() + + def _get_print_values(self, req_dict): + """ + Method to get all printing values. + + Returns + ------- + None + """ + logger("Getting all printing values...") + + default_vals = self.default_values.get('DEFAULT_PRINTING_WITH_OPTIONS_VALUES') + + ### PRINT VARS + print_media = None + print_color = None + print_sides = None + print_copies = None + print_ranges = None + + if req_dict.get('print_media') is not None: + print_media = req_dict['print_media'] + else: + print_media = default_vals.get('print_media') + + if req_dict.get('print_color') is not None: + print_color = req_dict['print_color'] + else: + print_color = default_vals.get('print_color') + + if req_dict.get('print_sides') is not None: + print_sides = req_dict['print_sides'] + else: + print_sides = default_vals.get('print_sides') + + if req_dict.get('print_copies') is not None: + print_copies = req_dict['print_copies'] + else: + print_copies = default_vals.get('print_copies') + + if req_dict.get('print_ranges') is not None: + print_ranges = req_dict['print_ranges'] + else: + print_ranges = default_vals.get('print_ranges') + + return { + 'print_media': print_media, + 'print_color': print_color, + 'print_sides': print_sides, + 'print_copies': print_copies, + 'print_ranges': print_ranges, + } + + def _get_counters_home_admin(self) -> dict: + """ + Method to get all counters for the /home-admin endpoint. + + Returns + ------- + dict: + The dictionary with all counter values. + """ + logger("Getting all counters for /home-admin...") + + return { + 'tot_uploads': self.usage.counter_uploads_get(), + 'tot_prints': self.usage.counter_prints_get(), + 'tot_pages': self.usage.counter_pages_get(), + 'tot_files_on_folder': self.usage.counter_files_get(), + 'tot_size': self.usage.counter_size_get(), + 'max_simultaneous_uploaded': self.usage.counter_max_simultaneous_uploaded_get(), + } + + def _get_counters_uploaded_list(self) -> dict: + """ + Method to get all counters for the /home-admin endpoint. + + Returns + ------- + dict: + The dictionary with all counter values. + """ + logger("Getting all counters for /home-admin...") + + return { + 'tot_uploaded_files': self.usage.counter_files_get(), + 'tot_folder_size': self.files.get_folder_bytes_size(), + } + ### (GET | POST) /home-admin @flask_login.login_required def home_admin(self) -> str: """ @@ -132,11 +245,6 @@ class AppRoutesProtected(): if 'messages' in request.args: msg_parsed = request.args['messages'] msg = get_message_sent(msg_parsed) - ### - ### WTF: find from where this is called - ### - # import pdb; pdb.set_trace() - # print("routes_protected.home_admin --> FROM REQUEST") ### FROM SESSION elif 'messages' in session.keys(): @@ -163,6 +271,9 @@ class AppRoutesProtected(): else: msg = None + ### GET COUNTERS + counter_values = self._get_counters_home_admin() + ### RENDER return render_template( 'home_admin.html', @@ -173,13 +284,15 @@ class AppRoutesProtected(): debug=self.config.debug, enabled_questions=self.config.enabled_questions, enabled_about=self.config.enabled_about, + enabled_api=self.config.enabled_api, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin, - tot_uploads=self.usage.counter_uploads_get(), - tot_prints=self.usage.counter_prints_get(), - tot_pages=self.usage.counter_pages_get(), - tot_files_on_folder=self.usage.get_total_files_in_folder(), - max_simultaneous_uploaded=self.usage.counter_max_simultaneous_uploaded_get(), + tot_uploads=counter_values.get('tot_uploads'), + tot_prints=counter_values.get('tot_prints'), + tot_pages=counter_values.get('tot_pages'), + tot_files_on_folder=counter_values.get('tot_files_on_folder'), + tot_size=counter_values.get('tot_size'), + max_simultaneous_uploaded=counter_values.get('max_simultaneous_uploaded'), msg=msg, alert_class=alert_class, from_request=from_request, @@ -187,6 +300,7 @@ class AppRoutesProtected(): auto_delete=self.config.file_auto_delete_after_print ) + ### (GET) /uploaded-list @flask_login.login_required def uploaded_list(self) -> str: """ @@ -201,10 +315,14 @@ class AppRoutesProtected(): ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('UPLOADED_LIST') + default_vals['page_h3'] = default_vals.get('page_h3').format(self.config.keep_file_minutes) ### GET ALL FILES LIST all_files = self.db_files.get_all() + ### GET COUNTERS + counter_values = self._get_counters_uploaded_list() + ### RENDER return render_template( 'upload_list.html', @@ -215,13 +333,17 @@ class AppRoutesProtected(): debug=self.config.debug, enabled_questions=self.config.enabled_questions, enabled_about=self.config.enabled_about, + enabled_api=self.config.enabled_api, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin, - # tot_uploaded_files=len(all_files), - tot_uploaded_files=self.usage.get_total_files_in_folder(), + tot_uploaded_files=counter_values.get('tot_uploaded_files'), + tot_folder_size=counter_values.get('tot_folder_size'), + # tot_uploaded_files=self.usage.counter_files_get(), + # tot_folder_size=self.files.get_folder_bytes_size(), uploaded_files=all_files ) + ### (GET | POST) /uploaded-clean --> POST -> /home-admin @flask_login.login_required def uploaded_clean(self): """ @@ -238,6 +360,7 @@ class AppRoutesProtected(): ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('UPLOADED_CLEAN') + default_vals['page_h3'] = default_vals.get('page_h3').format(self.config.keep_file_minutes) ### IF REQ GET if request.method == 'GET': @@ -253,6 +376,7 @@ class AppRoutesProtected(): debug=self.config.debug, enabled_questions=self.config.enabled_questions, enabled_about=self.config.enabled_about, + enabled_api=self.config.enabled_api, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin, files_expired=files_expired @@ -266,11 +390,8 @@ class AppRoutesProtected(): clean_files_lst = json.loads(req_dict['clean_files'].replace("'", '"')) ### DELETE FILES for file in clean_files_lst: + self._uploaded_clean_delete(file) files_deleted.append(file.get('file_code')) - ### DELETE FROM DB - self.db_files.file_delete(file_info=file) - ### DELETE FROM DISK - self.files.file_delete(file_info=file) ### CREATE SESSION VAR if len(files_deleted) == 0: @@ -290,7 +411,7 @@ class AppRoutesProtected(): return ret - ### UPDATED --> !!! DISABLED PRINT + ### (POST) /file-print-options --> /home-admin @flask_login.login_required def file_print_with_options(self): """ @@ -309,31 +430,12 @@ class AppRoutesProtected(): file_info = None file_code = None file_path = None - ### - print_media = None - print_color = None - print_sides = None - print_copies = None - print_ranges = None req_dict = request.form.to_dict() if 'file_code' in req_dict.keys(): ### GET PARSED file_code = req_dict['file_code'] - if req_dict.get('print_media') is not None: - print_media = req_dict['print_media'] - - if req_dict.get('print_color') is not None: - print_color = req_dict['print_color'] - - if req_dict.get('print_sides') is not None: - print_sides = req_dict['print_sides'] - - if req_dict.get('print_copies') is not None: - print_copies = req_dict['print_copies'] - - if req_dict.get('print_ranges') is not None: - print_ranges = req_dict['print_ranges'] + print_values = self._get_print_values(req_dict) ### GET FILE try: @@ -349,13 +451,13 @@ class AppRoutesProtected(): try: self.printer.print_with_options( file_path=file_path, - media=print_media, - color=print_color, - sides=print_sides, - page_ranges=print_ranges, - copies=print_copies + media=print_values.get('print_media'), + color=print_values.get('print_color'), + sides=print_values.get('print_sides'), + page_ranges=print_values.get('print_ranges'), + copies=print_values.get('print_copies') ) - logger_info(f"PRINT - file with code: '{file_code}'...") + logger_info(f"PRINTED - file with code: '{file_code}'...") except Exception as err: err_msg = f"Error printing the test file at the path: '{file_path}' with error: '{err}'" @@ -363,28 +465,40 @@ class AppRoutesProtected(): return err_msg else: - return "NOPE" + return "NOPE", 400 - ### UPDATE PRINT COUNTER - self.usage.counter_prints_update() + ### UPDATE USAGE + self.usage.update_from_print(file_info) ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('FILE_PRINT_OPTIONS') - ### RETURN INFO FROM DELETED FILE - ret_dict = { - "from": "file_print", - "msg": default_vals.get('ret_msg_with_file_code').format(file_code), - } - messages = json.dumps(ret_dict) - session['messages'] = messages - session['alert_class'] = default_vals.get('alert_class') - session['from_request'] = 'file_print' - session['file_code'] = file_code + ### RETURN INFO FROM PRINTED FILE + if self.config.debug_disable_printing: + ret_dict = { + "from": "file_print", + "msg": f"DEBUG PRINTING: 'debug_disable_printing' is set to True, not printing file with code: '{file_code}'", + } + messages = json.dumps(ret_dict) + session['messages'] = messages + session['alert_class'] = 'alert-warning' + session['from_request'] = 'file_print' + session['file_code'] = file_code + + else: + ret_dict = { + "from": "file_print", + "msg": default_vals.get('ret_msg_with_file_code').format(file_code), + } + messages = json.dumps(ret_dict) + session['messages'] = messages + session['alert_class'] = default_vals.get('alert_class') + session['from_request'] = 'file_print' + session['file_code'] = file_code return redirect(url_for('home-admin')) - ### UPDATED --> !!! DISABLED PRINT + ### (POST) /file-print --> /home-admin @flask_login.login_required def file_print(self): """ @@ -440,28 +554,38 @@ class AppRoutesProtected(): else: return "NOPE" - ### UPDATE PRINT COUNTER - self.usage.counter_prints_update() - - ### UPDATE TOTAL PAGES - self.usage.counter_pages_update(file_info) + ### UPDATE USAGE + self.usage.update_from_print(file_info) ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('FILE_PRINT') - ### RETURN INFO FROM DELETED FILE - ret_dict = { - "from": "file_print", - "msg": default_vals.get('ret_msg_with_file_code').format(file_code), - } - messages = json.dumps(ret_dict) - session['messages'] = messages - session['alert_class'] = default_vals.get('alert_class') - session['from_request'] = 'file_print' - session['file_code'] = file_code + ### RETURN INFO FROM PRINTED FILE + if self.config.debug_disable_printing: + ret_dict = { + "from": "file_print", + "msg": f"DEBUG PRINTING: 'debug_disable_printing' is set to True, not printing file with code: '{file_code}'", + } + messages = json.dumps(ret_dict) + session['messages'] = messages + session['alert_class'] = 'alert-warning' + session['from_request'] = 'file_print' + session['file_code'] = file_code + + else: + ret_dict = { + "from": "file_print", + "msg": default_vals.get('ret_msg_with_file_code').format(file_code), + } + messages = json.dumps(ret_dict) + session['messages'] = messages + session['alert_class'] = default_vals.get('alert_class') + session['from_request'] = 'file_print' + session['file_code'] = file_code return redirect(url_for('home-admin')) + ### (POST) /file-delete --> /home-admin @flask_login.login_required def file_delete(self): """ @@ -498,6 +622,9 @@ class AppRoutesProtected(): # From DB self.db_files.file_delete(file_info=founded_file) + ### UPDATE USAGE + self.usage.update_from_delete() + else: return "NOPE" @@ -515,6 +642,7 @@ class AppRoutesProtected(): return redirect(url_for('home-admin')) + ### (POST) /file-preview @flask_login.login_required def file_preview(self): """ @@ -553,6 +681,7 @@ class AppRoutesProtected(): debug=self.config.debug, enabled_questions=self.config.enabled_questions, enabled_about=self.config.enabled_about, + enabled_api=self.config.enabled_api, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin, max_upload_file_copies=self.config.max_upload_file_copies, @@ -567,6 +696,7 @@ class AppRoutesProtected(): return ret_preview + ### (POST) /remove-all-files --> /home-admin @flask_login.login_required def remove_all_files(self): """ @@ -597,6 +727,9 @@ class AppRoutesProtected(): logger_err(msg_err) return msg_err + ### DELETE USAGE + self.usage.remove_all_files() + ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('REMOVE_ALL_FILE') @@ -605,6 +738,7 @@ class AppRoutesProtected(): ### REDIRECT return redirect(url_for('home-admin')) + ### (GET) /docs/<path:filename> @flask_login.login_required def docs(self, filename): """ @@ -622,6 +756,7 @@ class AppRoutesProtected(): docs_dir = os.path.join(os.path.dirname(__file__), 'docs', 'html') return send_from_directory(directory=docs_dir, path=filename) + ### (GET) /about @flask_login.login_required def about(self): """ @@ -646,6 +781,7 @@ class AppRoutesProtected(): debug=self.config.debug, enabled_questions=self.config.enabled_questions, enabled_about=self.config.enabled_about, + enabled_api=self.config.enabled_api, user=flask_login.current_user.id, is_admin=flask_login.current_user.is_admin ) diff --git a/src/web/routes_user.py b/src/web/routes_user.py index c610e46ea8544c9abc8dccb624ac50b914dcc033..5642061612f2089e1c63856f8fdf2aa5eb8e4afb 100644 --- a/src/web/routes_user.py +++ b/src/web/routes_user.py @@ -222,11 +222,8 @@ class AppRoutesUser(): 'error_message': msg_err } - ### UPDATE UPLOADS COUNTER - self.usage.counter_uploads_update() - - ### UPDATE MAX_SIMULTANEOUS_UPLOADS COUNTER - self.usage.counter_max_simultaneous_uploaded_update() + ### UPDATE USAGE + self.usage.update_from_upload(file_info=ret_added_file_info) ### GET PAGE STYLE AND VARS default_vals = DEFAULT_ROUTE_VALS.get('UPLOAD') diff --git a/src/web/server.py b/src/web/server.py index c750ed87b3f197e5a19e931597dea53764fa2b89..ed9b61d8b859a1b18fc777e1e0eefd2207c1a6ba 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -4,13 +4,13 @@ In this file it will be set Flask server for uploading and printing files. """ - ### ### DEPENDENCIES ### import sys from flask import Flask import flask_login +from flask_jwt_extended import JWTManager ### LOCAL import web @@ -22,12 +22,6 @@ except ImportError as err: at the file 'web.server.py' file from project with error: {err}\n\n") sys.exit(1) -try: - from utils.app_usage import AppUsage -except ImportError as e: - print("\n\n Unable to import 'utils.app_usage.py' from the 'web.server.py' file from project \n\n") - sys.exit(1) - try: from utils.app_questions import AppQuestions except ImportError as e: @@ -73,7 +67,7 @@ class AppServer(): """ self._app = app - def __init__(self, config, printer, db_files, files) -> None: + def __init__(self, config, printer, db_files, files, usage) -> None: """ Method for initialiazing the FlaskServer class. Sets the class variables. @@ -87,10 +81,11 @@ class AppServer(): ------- None """ - self.db_files = db_files self.config = config self.printer = printer + self.db_files = db_files self.files = files + self.usage = usage # At the moment, only this two types of users are necessary # I didn't wanted to add an extra DB for dealing with logins... @@ -102,11 +97,15 @@ class AppServer(): self.config.login_admin_email: { 'password': self.config.login_admin_passwd, 'is_admin': True + }, + self.config.login_api_email: { + 'password': self.config.login_api_passwd, + 'is_admin': False } } - self.usage = AppUsage(self.config) - self.questions = AppQuestions(self.config) + if self.config.enabled_questions: + self.questions = AppQuestions(self.config) self.set_server() self.set_login_manager() @@ -131,16 +130,28 @@ class AppServer(): users=self.users ) + self.routes_api = None + if self.config.enabled_api: + self.routes_api = web.AppRoutesApi( + config=self.config, + jwt=self.jwt, + users=self.users, + usage=self.usage, + printer=self.printer + ) + self.routes_debug = web.AppRoutesDebug( config=self.config, users=self.users, printer=self.printer, + api=self.routes_api ) - self.routes_question = web.AppRoutesQuestion( - config=self.config, - questions=self.questions - ) + if self.config.enabled_questions: + self.routes_question = web.AppRoutesQuestion( + config=self.config, + questions=self.questions + ) self.set_routes() @@ -165,6 +176,9 @@ class AppServer(): self.login_manager = flask_login.LoginManager() self.login_manager.init_app(self.app) + ### JWT + self.jwt = JWTManager(self.app) + def set_login_manager(self) -> None: """ Method to set login manager. @@ -265,7 +279,6 @@ class AppServer(): view_func=self.routes_protected.file_print_with_options, methods=['POST'] ) - ### self.app.add_url_rule( rule='/file-preview', endpoint='file-preview', @@ -289,6 +302,59 @@ class AppServer(): view_func=self.routes_protected.docs, methods=['GET'] ) + + ############## + ############## API + ############## + + if self.config.enabled_api: + ### API - GRAPHANA + self.app.add_url_rule( + rule='/api', + view_func=self.routes_api.grafana_home, + methods=['GET'] + ) + self.app.add_url_rule( + rule='/api/metrics', + view_func=self.routes_api.grafana_metrics, + methods=['POST'] + ) + self.app.add_url_rule( + rule='/api/metric-payload-options', + view_func=self.routes_api.grafana_metric_payload_options, + methods=['POST'] + ) + self.app.add_url_rule( + rule='/api/query', + view_func=self.routes_api.grafana_query, + methods=['POST'] + ) + ### API - ENDPOINTS + self.app.add_url_rule( + rule='/api/create-login-token', + view_func=self.routes_api.create_login_token, + methods=['POST'] + ) + self.app.add_url_rule( + rule='/api/create-refresh-token', + view_func=self.routes_api.create_refresh_token, + methods=['POST'] + ) + self.app.add_url_rule( + rule='/api/refresh-token', + view_func=self.routes_api.refresh_created_token, + methods=['POST'] + ) + self.app.add_url_rule( + rule='/api/get-usage', + view_func=self.routes_api.get_usage, + methods=['GET'] + ) + self.app.add_url_rule( + rule='/api/test', + view_func=self.routes_api.test, + methods=['GET'] + ) ### DEBUG if self.config.debug: self.app.add_url_rule( @@ -298,9 +364,16 @@ class AppServer(): methods=['GET', 'POST'] ) self.app.add_url_rule( - rule='/debug-admin', - endpoint='debug-admin', - view_func=self.routes_debug.debug_admin, + rule='/debug-sys', + endpoint='debug-sys', + view_func=self.routes_debug.debug_sys, + methods=['GET'] + ) + if self.config.enabled_api: + self.app.add_url_rule( + rule='/debug-api', + endpoint='debug-api', + view_func=self.routes_debug.debug_api, methods=['GET'] ) ### QUESTIONS diff --git a/src/web/templates/debug_api.html b/src/web/templates/debug_api.html new file mode 100644 index 0000000000000000000000000000000000000000..f3fd20fe101ffa070766a46fb871755ca44840a4 --- /dev/null +++ b/src/web/templates/debug_api.html @@ -0,0 +1,71 @@ +{% extends 'layout_admin.html' %} + +{% block content %} + +<!-- SET SITE VALUES: Please don't change the source-code but insted use the config file for it, thank you --> +{% if page_texts %} +{% set page_h1 = page_texts.get('page_h1') %} +{% endif %} + +<!-- +---- +---- DISCLAIMER +---- +At the moment, not setting all texts as variables for this web-page. +I hope that in the future, a solution for the CUPS printer drivers can be founded... +I hate HP!!! +--> + +<!-- H1 --> +{% if page_h1 %} +<h1>{{ page_h1 }}</h1> +{% else %} +<h1>PLEASE SET TEXT FOR: page_h1</h1> +{% endif %} + + +<h3> + Here is the list from the API configuration: +</h3> +<br> + +<!-- LIST API CONFIG --> +{% if api_config %} + +<table class="table" style="width:100%"> + <thead class="thead-light"> + <tr> + <!-- KEY --> + <th> + KEY + </th> + <!-- VALUE --> + <th> + VALUE + </th> + </tr> + </thead> + <tbody> + {% for key, value in api_config.items() %} + <tr> + <td> + {{ key }} + </td> + <td> + {{ value }} + </td> + </tr> + {% endfor %} + <tbody> +</table> + +{% else %} +<div class="alert alert-danger" role="alert"> + <strong>Error!</strong> + <br> + The value from 'api_config' was not parsed to the page +</div> +{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/src/web/templates/debug_admin.html b/src/web/templates/debug_sys.html similarity index 98% rename from src/web/templates/debug_admin.html rename to src/web/templates/debug_sys.html index 2846bdd7e9027ecf977e55af85d3dc420c6e5bf2..fb50576f5523b696ac6071d8d4ddbe37374321bb 100644 --- a/src/web/templates/debug_admin.html +++ b/src/web/templates/debug_sys.html @@ -68,7 +68,7 @@ I hate HP!!! <hr> <h3> - Here is the list: + Here is the list with the system configuration: </h3> <br> @@ -111,5 +111,4 @@ I hate HP!!! {% endif %} - {% endblock %} \ No newline at end of file diff --git a/src/web/templates/home_admin.html b/src/web/templates/home_admin.html index 970b8700ebafd72b410f6b4f9c0262f7bcc4e2b6..441cdb67e4ecd2afc1c436b670cb458078db3aec 100644 --- a/src/web/templates/home_admin.html +++ b/src/web/templates/home_admin.html @@ -9,6 +9,7 @@ {% set text_total_prints = page_texts.get('text_total_prints') %} {% set text_total_pages = page_texts.get('text_total_pages') %} {% set text_total_files = page_texts.get('text_total_files') %} +{% set text_total_size = page_texts.get('text_total_size') %} {% set text_max_simultaneous_uploaded = page_texts.get('text_max_simultaneous_uploaded') %} {% set text_message = page_texts.get('text_message') %} {% set button_delete = page_texts.get('button_delete') %} @@ -40,7 +41,7 @@ <tr> <!-- TOTAL UPLOADS --> <th scope="row"> - {% if tot_uploads %} + {% if tot_uploads > 0 or tot_uploads == 0 %} <div class="d-flex justify-content-between"> <div> {% if text_total_uploads %} @@ -61,7 +62,7 @@ </th> <!-- TOTAL PRINTS --> <th scope="row"> - {% if tot_prints %} + {% if tot_prints > 0 or tot_prints == 0 %} <div class="d-flex justify-content-between"> <div> {% if text_total_prints %} @@ -105,7 +106,7 @@ </th> <!-- TOTAL PAGES --> <th scope="row"> - {% if tot_pages %} + {% if tot_pages > 0 or tot_pages == 0 %} <div class="d-flex justify-content-between"> <div> {% if text_total_pages %} @@ -128,7 +129,7 @@ <tr> <!-- MAX SIMULT UPLOADS --> <th scope="row"> - {% if max_simultaneous_uploaded %} + {% if max_simultaneous_uploaded > 0 or max_simultaneous_uploaded == 0 %} <div class="d-flex justify-content-between"> <div> {% if text_max_simultaneous_uploaded %} @@ -151,6 +152,32 @@ <th scope="row"> </th> </tr> + <tr> + <!-- TOTAL SIZE UPLOADED --> + <th scope="row"> + {% if tot_size > 0 or tot_size == 0 %} + <div class="d-flex justify-content-between"> + <div> + {% if text_total_size %} + <b>{{ text_total_size }}</b> + {% else %} + <b>PLEASE SET TEXT FOR: text_total_size</b> + {% endif %} + </div> + <div> + <span class="my_totals badge badge-pill text-right"> + {{ (tot_size / 1048576) | round(2) }} + </span> + </div> + </div> + {% else %} + NOT PARSED -> tot_size + {% endif %} + </th> + <!-- EMPTY: I LOVE FRONTEND SHIT --> + <th scope="row"> + </th> + </tr> </tbody> </table> diff --git a/src/web/templates/layout_admin.html b/src/web/templates/layout_admin.html index 3a15fa62d740526f482f8f3d3302c613a3551a2d..2ad41b39426a1aed9634e16a43ac007a10976710 100644 --- a/src/web/templates/layout_admin.html +++ b/src/web/templates/layout_admin.html @@ -12,7 +12,8 @@ {% set nav_bar_name_clean = admin_layout.get('nav_bar_name_clean') %} {% set nav_bar_name_questions = admin_layout.get('nav_bar_name_questions') %} {% set nav_bar_name_debug_printers = admin_layout.get('nav_bar_name_debug_printers') %} -{% set nav_bar_name_debug_admin = admin_layout.get('nav_bar_name_debug_admin') %} +{% set nav_bar_name_debug_system = admin_layout.get('nav_bar_name_debug_system') %} +{% set nav_bar_name_debug_api = admin_layout.get('nav_bar_name_debug_api') %} {% set nav_bar_name_printer_test = admin_layout.get('nav_bar_name_printer_test') %} {% set nav_bar_name_documentation = admin_layout.get('nav_bar_name_documentation') %} {% set nav_bar_name_logout = admin_layout.get('nav_bar_name_logout') %} @@ -149,6 +150,31 @@ </li> {% endif %} {% endif %} + <!-- + ### + ### ADMIN + ### + --> + {% if is_admin %} + <!-- DEBUG-API --> + {% if enabled_api %} + {% if enable_nav_link == 'debug-api' %} + <li class="nav-item active"> + <a class="nav-link disable" href=""> + <i class="fas fa-cogs"></i> + {{ nav_bar_name_debug_api }} + </a> + </li> + {% else %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('debug-api') }}"> + <i class="fas fa-cogs"></i> + {{ nav_bar_name_debug_api }} + </a> + </li> + {% endif %} + {% endif %} + {% endif %} <!-- ### ### DEBUG SESSION: This can only be seen if debug is enabled @@ -156,35 +182,35 @@ --> {% if debug %} {% if is_admin %} - <!-- DEBUG-PRINTERS --> - {% if enable_nav_link == 'debug-founded-printers' %} + <!-- DEBUG-SYSTEM --> + {% if enable_nav_link == 'debug-sys' %} <li class="nav-item active"> <a class="nav-link disable" href=""> - <i class="fas fa-print"></i> - {{ nav_bar_name_debug_printers }} + <i class="fas fa-cogs"></i> + {{ nav_bar_name_debug_system }} </a> </li> {% else %} <li class="nav-item"> - <a class="nav-link" href="{{ url_for('debug-printers') }}"> - <i class="fas fa-print"></i> - {{ nav_bar_name_debug_printers }} + <a class="nav-link" href="{{ url_for('debug-sys') }}"> + <i class="fas fa-cogs"></i> + {{ nav_bar_name_debug_system }} </a> </li> {% endif %} - <!-- DEBUG-SYSTEM --> - {% if enable_nav_link == 'debug-admin' %} + <!-- DEBUG-PRINTERS --> + {% if enable_nav_link == 'debug-founded-printers' %} <li class="nav-item active"> <a class="nav-link disable" href=""> - <i class="fas fa-cogs"></i> - {{ nav_bar_name_debug_admin }} + <i class="fas fa-print"></i> + {{ nav_bar_name_debug_printers }} </a> </li> {% else %} <li class="nav-item"> - <a class="nav-link" href="{{ url_for('debug-admin') }}"> - <i class="fas fa-cogs"></i> - {{ nav_bar_name_debug_admin }} + <a class="nav-link" href="{{ url_for('debug-printers') }}"> + <i class="fas fa-print"></i> + {{ nav_bar_name_debug_printers }} </a> </li> {% endif %} diff --git a/src/web/templates/upload_clean.html b/src/web/templates/upload_clean.html index fe58b4fff926b31312604a94432d25cc40aa7363..13d3c508905dd4dd23235b701e888745b5e513b0 100644 --- a/src/web/templates/upload_clean.html +++ b/src/web/templates/upload_clean.html @@ -5,11 +5,15 @@ <!-- SET SITE VALUES: Please don't change the source-code but insted use the config file for it, thank you --> {% if page_texts %} {% set page_h1 = page_texts.get('page_h1') %} +{% set page_h3 = page_texts.get('page_h3') %} +{% set text_file_files_expired = page_texts.get('text_file_files_expired') %} {% set text_total_files = page_texts.get('text_total_files') %} {% set text_file_code = page_texts.get('text_file_code') %} -{% set text_file_preview = page_texts.get('text_file_preview') %} +{% set text_file_delete = page_texts.get('text_file_delete') %} {% set text_file_date_upload = page_texts.get('text_file_date_upload') %} {% set text_file_original_name = page_texts.get('text_file_original_name') %} +{% set text_file_size = page_texts.get('text_file_size') %} +{% set text_file_pages = page_texts.get('text_file_pages') %} {% set button_delete_all = page_texts.get('button_delete_all') %} {% set button_nuke_all = page_texts.get('button_nuke_all') %} {% set button_delete = page_texts.get('button_delete') %} @@ -22,18 +26,65 @@ <h1>PLEASE SET TEXT FOR: page_h1</h1> {% endif %} -<!-- BUTTON DELETE ALL--> -<form action='uploaded-clean' method='POST'> - <button class="btn btn-primary float-left" type='submit' name='clean_files' value="{{ files_expired }}"> - {% if button_delete_all %} - {{ button_delete_all }} - {% else %} - PLEASE SET TEXT FOR: button_delete_all - {% endif %} - </button> -</form> +<!-- DEBUG --> +{% if debug %} + +<div class="alert alert-danger" role="alert" style="width:50%"> + <strong>DEBUG!</strong> + <br> + <strong>Please disable debug in production!!!</strong> + <br> + <!-- USER --> + {% if user %} + <p>DEBUGGING -> Logged in as: {{ user }}</p> + <p>DEBUGGING -> Logged is admin: '{{ is_admin }}'</p> + {% endif %} +</div> + +{% endif %} + +<!-- H3 --> +{% if page_h3 %} +<h3>{{ page_h3 }}</h3> +{% else %} +<h3>PLEASE SET TEXT FOR: page_h3</h3> +{% endif %} + +<table class="table" style="width:30%"> + <tbody> + <tr> + <!-- TOTAL FILES EXPIRED --> + {% if files_expired|length > 0 or files_expired|length == 0 %} + <th> + {% if text_file_files_expired %} + {{ text_file_files_expired }} + <span class="my_totals badge badge-pill"> + {{ files_expired|length }} + </span> + {% else %} + PLEASE SET TEXT FOR -> text_file_files_expired: {{ tot_folder_size }} + {% endif %} + </th> + {% endif %} + <!-- BUTTON DELETE ALL--> + <th> + <form action='uploaded-clean' method='POST'> + <button class="btn btn-primary float-left" type='submit' name='clean_files' value="{{ files_expired }}"> + {% if button_delete_all %} + {{ button_delete_all }} + {% else %} + PLEASE SET TEXT FOR: button_delete_all + {% endif %} + </button> + </form> + </th> + </tr> + </tbody> +</table> + <!-- BUTTON NUKE ALL--> {% if is_admin %} +<br> <form action='remove-all-files' method='POST'> <button class="btn btn-danger float-right" type='submit' name='nuke_files' value="{{ files_expired }}"> {% if button_nuke_all %} @@ -47,37 +98,68 @@ </form> {% endif %} +<!-- TABLE WITH LIST OF FILES --> <table class="table" style="width:100%"> <thead class="thead-light"> <tr> <!-- FILE CODE --> {% if text_file_code %} - <th>{{ text_file_code }}</th> + <th> + <i class="fas fa-file-code"></i> + {{ text_file_code }} + </th> {% else %} <th>PLEASE SET TEXT FOR: text_file_code</th> {% endif %} - <!-- FILE PREVIEW --> - {% if text_file_preview %} - <th>{{ text_file_preview }}</th> + <!-- FILE DELETE --> + {% if text_file_delete %} + <th> + <i class="fas fa-trash"></i> + {{ text_file_delete }} + </th> {% else %} - <th>PLEASE SET TEXT FOR: text_file_preview</th> + <th>PLEASE SET TEXT FOR: text_file_delete</th> {% endif %} <!-- DATE UPLOAD --> {% if text_file_date_upload %} - <th>{{ text_file_date_upload }}</th> + <th> + <i class="fas fa-calendar"></i> + {{ text_file_date_upload }} + </th> {% else %} <th>PLEASE SET TEXT FOR: text_file_date_upload</th> {% endif %} <!-- ORIG FILE NAME --> {% if text_file_original_name %} - <th>{{ text_file_original_name }}</th> + <th> + <i class="fas fa-user-tag"></i> + {{ text_file_original_name }} + </th> {% else %} <th>PLEASE SET TEXT FOR: text_file_original_name</th> {% endif %} <!-- DEBUG --> {% if debug %} - <th>New File Name</th> - <th>File Path</th> + <th style="color:#FF0000">New File Name</th> + <th style="color:#FF0000">File Path</th> + {% endif %} + <!-- FILE SIZE --> + {% if text_file_size %} + <th> + <i class="fas fa-hdd"></i> + {{ text_file_size }} + </th> + {% else %} + <th>PLEASE SET TEXT FOR: text_file_size</th> + {% endif %} + <!-- FILE PAGES --> + {% if text_file_pages %} + <th> + <i class="fas fa-copy"></i> + {{ text_file_pages }} + </th> + {% else %} + <th>PLEASE SET TEXT FOR: text_file_pages</th> {% endif %} </tr> </thead> @@ -110,8 +192,10 @@ {% if debug %} <td>{{ value }}</td> {% endif %} + {% elif key == "size" %} + <td>{{ (value / 1048576) | round(2) }}</td> {% else %} - <td>{{ value }}</td> + <td>{{ value }}</td> {% endif %} {% endfor %} </tr> diff --git a/src/web/templates/upload_list.html b/src/web/templates/upload_list.html index b59488ec6fbda5034f12cddc72abc2c6d7fa4813..cc6566ba1fe2e6d4fc8071c97d46892dcea26c41 100644 --- a/src/web/templates/upload_list.html +++ b/src/web/templates/upload_list.html @@ -5,11 +5,15 @@ <!-- SET SITE VALUES: Please don't change the source-code but insted use the config file for it, thank you --> {% if page_texts %} {% set page_h1 = page_texts.get('page_h1') %} +{% set page_h3 = page_texts.get('page_h3') %} {% set text_total_files = page_texts.get('text_total_files') %} +{% set text_total_folder_size = page_texts.get('text_total_folder_size') %} {% set text_file_code = page_texts.get('text_file_code') %} {% set text_file_preview = page_texts.get('text_file_preview') %} {% set text_file_date_upload = page_texts.get('text_file_date_upload') %} {% set text_file_original_name = page_texts.get('text_file_original_name') %} +{% set text_file_size = page_texts.get('text_file_size') %} +{% set text_file_pages = page_texts.get('text_file_pages') %} {% set button_preview = page_texts.get('button_preview') %} {% endif %} @@ -22,7 +26,6 @@ <!-- DEBUG --> {% if debug %} - <div class="alert alert-danger" role="alert" style="width:50%"> <strong>DEBUG!</strong> <br> @@ -34,13 +37,20 @@ <p>DEBUGGING -> Logged is admin: '{{ is_admin }}'</p> {% endif %} </div> +{% endif %} +<!-- H3 --> +{% if page_h3 %} +<h3>{{ page_h3 }}</h3> +{% else %} +<h3>PLEASE SET TEXT FOR: page_h3</h3> {% endif %} -{% if tot_uploaded_files %} +<!-- TOTAL UPLOADS --> +{% if tot_uploaded_files > 0 or tot_uploaded_files == 0 %} <p> {% if text_total_files %} - {{ text_total_files }}: + {{ text_total_files }} <span class="my_totals badge badge-pill"> {{ tot_uploaded_files }} </span> @@ -50,6 +60,20 @@ </p> {% endif %} +<!-- TOTAL FOLDER SIZE --> +{% if tot_folder_size > 0 or tot_folder_size == 0 %} +<p> + {% if text_total_folder_size %} + {{ text_total_folder_size }} + <span class="my_totals badge badge-pill"> + {{ (tot_folder_size / 1048576) | round(2) }} + </span> + {% else %} + PLEASE SET TEXT FOR -> text_total_folder_size: {{ tot_folder_size }} + {% endif %} +</p> +{% endif %} + <table class="table" style="width:100%"> <thead class="thead-light"> <tr> @@ -65,7 +89,7 @@ <!-- FILE PREVIEW --> {% if text_file_preview %} <th> - <i class="fas fa-search"></i> + <i class="fas fa-image"></i> {{ text_file_preview }} </th> {% else %} @@ -94,6 +118,24 @@ <th style="color:#FF0000">New File Name</th> <th style="color:#FF0000">Full Path</th> {% endif %} + <!-- FILE SIZE --> + {% if text_file_size %} + <th> + <i class="fas fa-hdd"></i> + {{ text_file_size }} + </th> + {% else %} + <th>PLEASE SET TEXT FOR: text_file_size</th> + {% endif %} + <!-- FILE PAGES --> + {% if text_file_pages %} + <th> + <i class="fas fa-copy"></i> + {{ text_file_pages }} + </th> + {% else %} + <th>PLEASE SET TEXT FOR: text_file_pages</th> + {% endif %} </tr> </thead> <tbody> @@ -125,6 +167,8 @@ {% if debug %} <td>{{ value }}</td> {% endif %} + {% elif key == "size" %} + <td>{{ (value / 1048576) | round(2) }}</td> {% else %} <td>{{ value }}</td> {% endif %} diff --git a/uploads/429-359-943___-___2025-01-15_-_20-01-36___-___test_file_1.pdf b/uploads/429-359-943___-___2025-01-15_-_20-01-36___-___test_file_1.pdf deleted file mode 100644 index b3987e27a6680219d10144aba89575ac8470ecca..0000000000000000000000000000000000000000 Binary files a/uploads/429-359-943___-___2025-01-15_-_20-01-36___-___test_file_1.pdf and /dev/null differ