Compare commits
	
		
			No commits in common. "eaeecc926a5e0fe31df2006d0614a573a510f9c0" and "08380b2ca3e3767cecdcbd878c70db626175e66f" have entirely different histories.
		
	
	
		
			eaeecc926a
			...
			08380b2ca3
		
	
		
							
								
								
									
										13
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -1,13 +0,0 @@ | |||||||
| # Changelog |  | ||||||
| 
 |  | ||||||
| ## [0.1.0] - 2023-09-16 |  | ||||||
| ### Added |  | ||||||
| - Web interface |  | ||||||
| - Fully featured RestFullAPI v1; |  | ||||||
| - Monitoring free space in storage; |  | ||||||
| - Deleting an archive or ticket also deletes physical files; |  | ||||||
| - Flexible deployment configuration using environment variables; |  | ||||||
| - Dockerized app, the image size is less than 150mb; |  | ||||||
| - Support sqlite3 and PostgreSQL^15; |  | ||||||
| - Whitenoise Static management; |  | ||||||
| - healthcheck checking application availability; |  | ||||||
							
								
								
									
										324
									
								
								README-ru.md
									
									
									
									
									
								
							
							
						
						
									
										324
									
								
								README-ru.md
									
									
									
									
									
								
							| @ -1,324 +0,0 @@ | |||||||
| # LOGS-COLLECTOR |  | ||||||
| 
 |  | ||||||
| ```sh |  | ||||||
| █░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█ |  | ||||||
| █▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄ |  | ||||||
| ``` |  | ||||||
| ###  [English lang: README.md](README.md) |  | ||||||
| 
 |  | ||||||
| ###  [CHANGELOG.md](CHANGELOG.md) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Цель |  | ||||||
| 
 |  | ||||||
| Если вы являетесь разработчиком ПО которое в дальнейшем клиенты используют в своей инфраструктуре, вы должны понимать, как иногда бывает трудно изучить проблему с ПО не имея доступа к серверу на котором это ПО работает. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| Для решения этой задачи вы можете настраивать ПО на автоматическую отправку обезличенных отчетов о сбоях например использовать Sentry. Это не всегда приемлемо для клиента, к тому же информация может быть не полной или клиенту требуется повышенная конфиденциальность. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| В таком случае вы можете попросить клиента отправить вам нужные лог файлы и изучить их в последствии. Но тут возникает другая проблема вам нужен безопасный способ передачи этих файлов как для вас так и для клиента. |  | ||||||
| Это мог быть FTP, SFTP, облако etc. Но что если вы не хотите давать клиенту данные для аутентификации и авторизации? |  | ||||||
| 
 |  | ||||||
| Возможно у вас есть доступ к серверу клиента и вы можете прочитать лог файлы на месте. И казалось бы проблема решена. Но на сервере клиента могут отсутствовать инструменты для удобного изучения лог файлов. |  | ||||||
| Даже если сотрудник поддержки может забрать себе нужные файлы и изучить их локально, возникает проблема распространения этих файлов между другими сотрудниками. |  | ||||||
| 
 |  | ||||||
| Logs-collector позволяет решить эти задачи. |  | ||||||
| 
 |  | ||||||
| Logs-collector является удаленным хранилищем и может принимать и отдавать файлы. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Термины |  | ||||||
| - Платформа: это ПО разработанное вашей компанией |  | ||||||
| - Тикет: это номер связанный с тикетом в вашей help desk системе |  | ||||||
| - Архив: это загруженный лог файл (поддерживается любой формат) |  | ||||||
| 
 |  | ||||||
| ## Как это работает? |  | ||||||
| 
 |  | ||||||
| - Создаете платформы |  | ||||||
| - Создаете тикет связанный с платформой и номером  |  | ||||||
| - Передаете клиенту уникальный токен тикета |  | ||||||
| - Клиент загружает архив лог файлов |  | ||||||
| - Скачиваете архив (находите решение проблемы) |  | ||||||
| - Удаляете архив или тикет или отмечаете тикет решенным |  | ||||||
| 
 |  | ||||||
| ## Особенности |  | ||||||
| 
 |  | ||||||
| - Централизованное хранилище |  | ||||||
| - Для загрузки файла не нужно давать auth credentials |  | ||||||
| - Каждый токен на загрузку уникален и связан только с одним тикетом |  | ||||||
| - Токен имеет ограничение на количество попыток и время жизни |  | ||||||
| - Загрузить файл можно из консоли или через веб |  | ||||||
| - Полнофункциональный RestFullAPI v1 |  | ||||||
| - Мониторинг свободного пространства в хранилище |  | ||||||
| - Удаление архива или тикета так же удаляет физические файлы |  | ||||||
| - Приложение соответствует архитектуре приложения 12 факторов |  | ||||||
| - Гибкая настройка развертывания переменными окружения |  | ||||||
| - Приложение докеризировано, размер образа меньше 150mb |  | ||||||
| - Может работать как с sqlite3 так и с PostgreSQL^15 |  | ||||||
| - Управление статикой без настройки для этого веб сервера |  | ||||||
| - healthcheck проверка доступности приложения |  | ||||||
| 
 |  | ||||||
| ## Безопасность |  | ||||||
| 
 |  | ||||||
| - Токен на загрузку не связан с авторизацией |  | ||||||
| - Токен на загрузку обладает высокой энтропией. |  | ||||||
| - Двухфакторная аутентификация для пользователей |  | ||||||
| - Для скачивания файла - 2FA должна быть принудительно включена |  | ||||||
| - Админ панель пропатчена на принудительное использование 2FA |  | ||||||
| - Пользователь в контейнере является не привилегированным  |  | ||||||
| - Стандартные методы защиты Django и DRF |  | ||||||
| 
 |  | ||||||
| ## Установка |  | ||||||
| 
 |  | ||||||
| ### Из docker образа: |  | ||||||
| - Создайте директорию для приложения где вам удобно |  | ||||||
| - Создайте файл docker-compose.yml в директории приложения |  | ||||||
| - Создайте файл .env в директории приложения |  | ||||||
| - Наполните файл .env требуемыми переменными окружения см. ниже |  | ||||||
| 
 |  | ||||||
| >Пример файла с использованием хранилища докер и sqlite как база данных по умолчанию: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| version: "3" |  | ||||||
| 
 |  | ||||||
| # to set environment variables: |  | ||||||
| # create a .env file in the same directory as docker-compose.yaml |  | ||||||
| 
 |  | ||||||
| services: |  | ||||||
|   server: |  | ||||||
|     image: mois3y/logs_collector:0.1.0 |  | ||||||
|     container_name: logs-collector |  | ||||||
|     restart: unless-stopped |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
|     ports: |  | ||||||
|       - "80:8000" |  | ||||||
|     volumes: |  | ||||||
|       - /etc/timezone:/etc/timezone:ro  # optional |  | ||||||
|       - /etc/localtime:/etc/localtime:ro  # optional |  | ||||||
|       - logs_collector_data:/data |  | ||||||
| 
 |  | ||||||
| volumes: |  | ||||||
|   logs_collector_data: |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Из исходников: |  | ||||||
| - Клонируйте репозиторий |  | ||||||
| - docker-compose.yaml уже есть в директории с проектом |  | ||||||
| - создайте в корне проекта файл .env |  | ||||||
| - наполните .env требуемыми переменными окружения см. ниже |  | ||||||
| - соберите образ и запустите контейнер в фоне: |  | ||||||
|    |  | ||||||
| ```sh |  | ||||||
| docker-compose up -d --build |  | ||||||
| ``` |  | ||||||
| - Вы можете создать свой файл и внести нужные правки: |  | ||||||
| #### docker-compose-example-psql.yaml c PostgreSQL по умолчанию: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| services: |  | ||||||
|   logs_collector: |  | ||||||
|     container_name: logs-collector |  | ||||||
|     build: |  | ||||||
|       context: . |  | ||||||
|       args: |  | ||||||
|         - VERSION=${VERSION} |  | ||||||
|         - SRC_DIR=${SRC_DIR} |  | ||||||
|         - SCRIPTS_DIR=${SCRIPTS_DIR} |  | ||||||
|         - APP_DIR=${APP_DIR} |  | ||||||
|         - DATA_DIR=${DATA_DIR} |  | ||||||
|         - WEB_PORT=${WEB_PORT} |  | ||||||
|         - USER_NAME=${USER_NAME} |  | ||||||
|         - USER_GROUP=${USER_GROUP} |  | ||||||
|         - APP_UID=${APP_UID} |  | ||||||
|         - APP_GID=${APP_GID} |  | ||||||
|     ports: |  | ||||||
|       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" |  | ||||||
|     volumes: |  | ||||||
|       - type: volume |  | ||||||
|         source: logs_collector_data |  | ||||||
|         target: ${APP_DIR}/data |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
|     depends_on: |  | ||||||
|       - db |  | ||||||
|        |  | ||||||
|   db: |  | ||||||
|     image: postgres:15-alpine3.18 |  | ||||||
|     container_name: psql-collector |  | ||||||
|     volumes: |  | ||||||
|       - logs_collector_psql_data:/var/lib/postgresql/data/ |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| volumes: |  | ||||||
|   logs_collector_data: |  | ||||||
|   logs_collector_psql_data: |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| #### docker-compose-example-psql.yaml c sqlite и bind-mount: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| version: "3" |  | ||||||
| 
 |  | ||||||
| # to set environment variables: |  | ||||||
| # create a .env file in the same directory as docker-compose.yaml |  | ||||||
| 
 |  | ||||||
| services: |  | ||||||
|   logs_collector: |  | ||||||
|     container_name: logs-collector |  | ||||||
|     build: |  | ||||||
|       context: . |  | ||||||
|       args: |  | ||||||
|         - VERSION=${VERSION} |  | ||||||
|         - SRC_DIR=${SRC_DIR} |  | ||||||
|         - SCRIPTS_DIR=${SCRIPTS_DIR} |  | ||||||
|         - APP_DIR=${APP_DIR} |  | ||||||
|         - DATA_DIR=${DATA_DIR} |  | ||||||
|         - WEB_PORT=${WEB_PORT} |  | ||||||
|         - USER_NAME=${USER_NAME} |  | ||||||
|         - USER_GROUP=${USER_GROUP} |  | ||||||
|         - APP_UID=${APP_UID} |  | ||||||
|         - APP_GID=${APP_GID} |  | ||||||
|     ports: |  | ||||||
|       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" |  | ||||||
|     volumes: |  | ||||||
|       - "/opt/collector/data:${DATA_DIR}" |  | ||||||
|       - "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3" |  | ||||||
|     env_file: |  | ||||||
|       - /.env |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 🔴 |  | ||||||
| 
 |  | ||||||
| ❗ВАЖНО❗ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| Если вы используете bind-mount и монтируете его в хранилище приложения, помните |  | ||||||
| пользователь в контейнере не привилегирован UID 1000 если примонтированный файл |  | ||||||
| или директория будет принадлежать root приложение не сможет его прочитать и |  | ||||||
| следовательно работать. |  | ||||||
| 
 |  | ||||||
| В продакшн среде используйте приложение за вашим любимым обратным прокси. |  | ||||||
| 
 |  | ||||||
| Просто добавьте его в стек docker-compose.yaml |  | ||||||
| 
 |  | ||||||
| >Можно этого не делать, но Gunicorn рекомендуют придерживаться этого правила. |  | ||||||
| > |  | ||||||
| >Я солидарен с ними, так что вас предупредили) |  | ||||||
| 
 |  | ||||||
| 🔴 |  | ||||||
| 
 |  | ||||||
| ## Переменные окружения: |  | ||||||
| >Приложение можно настроить, для этого передайте следующие возможные переменные |  | ||||||
| >окружения. |  | ||||||
| >Если переменная не передана, будет использоваться переменная окружения по умолчанию |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
|  █▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀ |  | ||||||
|  █▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| | ENV                  | DEFAULT         | INFO                     | |  | ||||||
| | -------------------- | --------------- | ------------------------ | |  | ||||||
| | SECRET_KEY           | j9QGbvM9Z4otb47 | ❗change this immediately| |  | ||||||
| | DEBUG                | False           | use only False in prod   | |  | ||||||
| | ALLOWED_HOSTS        | '*'             | list separated by commas | |  | ||||||
| | CSRF_TRUSTED_ORIGINS |                 | list separated by commas | |  | ||||||
| | DB_URL               |                 | url for connect db       | |  | ||||||
| | TZ                   | 'UTC'           | server timezone          | |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| [CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins) |  | ||||||
| 
 |  | ||||||
| Требуется в среде докер в продакшн окружении |  | ||||||
| принимает список url разделенных запятой |  | ||||||
| >http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| [DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html) |  | ||||||
| 
 |  | ||||||
| Нужно указывать если вы хотите использовать PostgreSQL |  | ||||||
| Эти данные должны совпадать с переменными контейнера PostgreSQL |  | ||||||
| 
 |  | ||||||
| | ENV               | VALUE          | |  | ||||||
| | ----------------- | -------------- | |  | ||||||
| | POSTGRES_USER     | admin          | |  | ||||||
| | POSTGRES_PASSWORD | ddkwndkjdX7RrP | |  | ||||||
| | POSTGRES_DB       | collector      | |  | ||||||
| 
 |  | ||||||
| Пример: |  | ||||||
| 
 |  | ||||||
| #### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector |  | ||||||
| - Протокол: **psql://** |  | ||||||
| - Пользователь: **admin** |  | ||||||
| - Пароль: **ddkwndkjdX7RrP** |  | ||||||
| - IP адрес: **psql-collector** |  | ||||||
| - Порт: **5432** |  | ||||||
| - Имя БД: **collector** |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| █▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀ |  | ||||||
| █▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| | ENV                         | DEFAULT        | |  | ||||||
| | --------------------------- | -------------- | |  | ||||||
| | GUNICORN_BIND               | '0.0.0.0:8000' | |  | ||||||
| | GUNICORN_BACKLOG            | 2048           | |  | ||||||
| | GUNICORN_WORKERS            | 2              | |  | ||||||
| | GUNICORN_WORKER_CLASS       | 'sync'         | |  | ||||||
| | GUNICORN_WORKER_CONNECTIONS | 1000           | |  | ||||||
| | GUNICORN_THREADS            | 1              | |  | ||||||
| | GUNICORN_TIMEOUT            | 3600           | |  | ||||||
| | GUNICORN_KEEPALIVE          | 2              | |  | ||||||
| | GUNICORN_LOGLEVEL           | 'info'         | |  | ||||||
| 
 |  | ||||||
| [GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html) |  | ||||||
| 
 |  | ||||||
| Подробная информация о каждой переменной окружения доступна в официальной документации. |  | ||||||
| 
 |  | ||||||
| GUNICORN_BIND не изменяйте это так как переменная отвечает за прослушиваемый адрес и порт внутри контейнера. |  | ||||||
| 
 |  | ||||||
| GUNICORN_TIMEOUT по умолчанию установлена в 3600. Такой большой таймаут нужен для загрузки больших файлов. |  | ||||||
| Поскольку я старался сделать приложение минималистичным и не использовать менеджер задач загрузка файла идет в один поток. |  | ||||||
| 
 |  | ||||||
| Если время загрузки будет больше часа соединение разорвется, это особенность синхронной работы воркеров gunicorn если вам не хватает времени на загрузку вы можете увеличить это значение. |  | ||||||
| 
 |  | ||||||
| ❗ВАЖНО❗ |  | ||||||
| 
 |  | ||||||
| Gunicorn настроен писать в лог в следующем формате: |  | ||||||
| ```python |  | ||||||
| '%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' |  | ||||||
| ``` |  | ||||||
| Это значит что в логе будет видно IP адрес запроса только из заголовка |  | ||||||
| 
 |  | ||||||
| **X-Forwarded-For** |  | ||||||
| 
 |  | ||||||
| В продакшн среде приложение должно быть за обратным прокси |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Помощники |  | ||||||
| В корне репозитория проекта есть директория scripts в ней лежит скрипт uploader.sh с помощью которого можно отправить файлы из консоли используя curl. |  | ||||||
| 
 |  | ||||||
| Синтаксис простой: |  | ||||||
| 
 |  | ||||||
| ```cmd |  | ||||||
| Usage: ./uploader.sh [options [parameters]] |  | ||||||
| 
 |  | ||||||
| Options: |  | ||||||
| 
 |  | ||||||
|  -f | --file     full path to upload file required |  | ||||||
|  -t | --token    access token             required |  | ||||||
|  -u | --url      target url               required |  | ||||||
|  -v | --version  print version |  | ||||||
|  -h | --help     print help |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Лицензия |  | ||||||
| 
 |  | ||||||
| GNU GPL 3.0 |  | ||||||
							
								
								
									
										330
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										330
									
								
								README.md
									
									
									
									
									
								
							| @ -1,329 +1,3 @@ | |||||||
| # LOGS-COLLECTOR | # logs-collector | ||||||
| 
 | 
 | ||||||
| ```sh | Серверная сторона для получения и хранения лог файлов | ||||||
| █░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█ |  | ||||||
| █▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ###  [CHANGELOG.md](CHANGELOG.md) |  | ||||||
| 
 |  | ||||||
| ###  [Russian lang: README.md](README-ru.md) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Purpose |  | ||||||
| 
 |  | ||||||
| If you are a developer of software that clients later use in their infrastructure, |  | ||||||
| you must understand how sometimes it can be difficult to research a problem |  | ||||||
| with software without access to the server on which this software runs. |  | ||||||
| 
 |  | ||||||
| To solve this problem, you can configure the software to automatically send  |  | ||||||
| anonymized crash reports, for example, use Sentry.  |  | ||||||
| This is not always acceptable to the client;  |  | ||||||
| Moreover, the information may not be complete or the client  |  | ||||||
| requires increased confidentiality. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Terms |  | ||||||
| - Platform: this is software developed by your company |  | ||||||
| - Ticket: this is the number associated with the ticket in your help desk system |  | ||||||
| - Archive: this is an uploaded log file (any format is supported) |  | ||||||
| 
 |  | ||||||
| ## How it works? |  | ||||||
| 
 |  | ||||||
| - Create platforms |  | ||||||
| - Create a ticket associated with the platform and number |  | ||||||
| - Transfer a unique ticket token to the client |  | ||||||
| - The client downloads an archive of log files |  | ||||||
| - Download the archive (find a solution to the problem) |  | ||||||
| - Delete the archive or ticket or mark the ticket as resolved |  | ||||||
|    |  | ||||||
| ## Features |  | ||||||
| 
 |  | ||||||
| - Centralized storage; |  | ||||||
| - To download a file you do not need to provide auth credentials; |  | ||||||
| - Each download token is unique and associated with only one ticket; |  | ||||||
| - The token has a limit on the number of attempts and lifetime; |  | ||||||
| - You can download the file from the console or via the web; |  | ||||||
| - Fully featured RestFullAPI v1; |  | ||||||
| - Monitoring free space in storage; |  | ||||||
| - Deleting an archive or ticket also deletes physical files; |  | ||||||
| - The application follows the 12 factors application architecture; |  | ||||||
| - Flexible deployment configuration using environment variables; |  | ||||||
| - The application is dockerized, the image size is less than 150mb; |  | ||||||
| - Can work with both sqlite3 and PostgreSQL^15; |  | ||||||
| - Static management without configuration for this web server; |  | ||||||
| - healthcheck checking application availability; |  | ||||||
| 
 |  | ||||||
| ## Security |  | ||||||
| 
 |  | ||||||
| - The download token is not associated with authorization |  | ||||||
| - The download token has high entropy. |  | ||||||
| - Two-factor authentication for users |  | ||||||
| - To download a file - 2FA must be forcibly enabled |  | ||||||
| - The admin panel has been patched to force the use of 2FA |  | ||||||
| - The user in the container is not privileged |  | ||||||
| - Standard Django and DRF protection methods |  | ||||||
| 
 |  | ||||||
| ## Install |  | ||||||
| 
 |  | ||||||
| ### From the docker image: |  | ||||||
| - Create a directory for the application wherever it is convenient for you |  | ||||||
| - Create a docker-compose.yml file in the application directory |  | ||||||
| - Create a .env file in the application directory |  | ||||||
| - Fill the .env file with the required environment variables, see below |  | ||||||
| 
 |  | ||||||
| >Example file using docker store and sqlite as default database: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| version: "3" |  | ||||||
| 
 |  | ||||||
| # to set environment variables: |  | ||||||
| # create a .env file in the same directory as docker-compose.yaml |  | ||||||
| 
 |  | ||||||
| services: |  | ||||||
|   server: |  | ||||||
|     image: mois3y/logs_collector:0.1.0 |  | ||||||
|     container_name: logs-collector |  | ||||||
|     restart: unless-stopped |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
|     ports: |  | ||||||
|       - "80:8000" |  | ||||||
|     volumes: |  | ||||||
|       - /etc/timezone:/etc/timezone:ro  # optional |  | ||||||
|       - /etc/localtime:/etc/localtime:ro  # optional |  | ||||||
|       - logs_collector_data:/data |  | ||||||
| 
 |  | ||||||
| volumes: |  | ||||||
|   logs_collector_data: |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### From the source: |  | ||||||
| - Clone the repository |  | ||||||
| - docker-compose.yaml is already in the project directory |  | ||||||
| - create a .env file in the project root |  | ||||||
| - fill .env with the required environment variables, see below |  | ||||||
| - build the image and run the container in the background: |  | ||||||
|    |  | ||||||
| ```sh |  | ||||||
| docker-compose up -d --build |  | ||||||
| ``` |  | ||||||
| - You can create your own file and make the necessary edits: |  | ||||||
| #### docker-compose.yaml PostgreSQL by default: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| services: |  | ||||||
|   logs_collector: |  | ||||||
|     container_name: logs-collector |  | ||||||
|     build: |  | ||||||
|       context: . |  | ||||||
|       args: |  | ||||||
|         - VERSION=${VERSION} |  | ||||||
|         - SRC_DIR=${SRC_DIR} |  | ||||||
|         - SCRIPTS_DIR=${SCRIPTS_DIR} |  | ||||||
|         - APP_DIR=${APP_DIR} |  | ||||||
|         - DATA_DIR=${DATA_DIR} |  | ||||||
|         - WEB_PORT=${WEB_PORT} |  | ||||||
|         - USER_NAME=${USER_NAME} |  | ||||||
|         - USER_GROUP=${USER_GROUP} |  | ||||||
|         - APP_UID=${APP_UID} |  | ||||||
|         - APP_GID=${APP_GID} |  | ||||||
|     ports: |  | ||||||
|       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" |  | ||||||
|     volumes: |  | ||||||
|       - type: volume |  | ||||||
|         source: logs_collector_data |  | ||||||
|         target: ${APP_DIR}/data |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
|     depends_on: |  | ||||||
|       - db |  | ||||||
|        |  | ||||||
|   db: |  | ||||||
|     image: postgres:15-alpine3.18 |  | ||||||
|     container_name: psql-collector |  | ||||||
|     volumes: |  | ||||||
|       - logs_collector_psql_data:/var/lib/postgresql/data/ |  | ||||||
|     env_file: |  | ||||||
|       - ./.env |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| volumes: |  | ||||||
|   logs_collector_data: |  | ||||||
|   logs_collector_psql_data: |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| #### docker-compose-example-psql.yaml c sqlite и bind-mount: |  | ||||||
| 
 |  | ||||||
| ```yaml |  | ||||||
| version: "3" |  | ||||||
| 
 |  | ||||||
| # to set environment variables: |  | ||||||
| # create a .env file in the same directory as docker-compose.yaml |  | ||||||
| 
 |  | ||||||
| services: |  | ||||||
|   logs_collector: |  | ||||||
|     container_name: logs-collector |  | ||||||
|     build: |  | ||||||
|       context: . |  | ||||||
|       args: |  | ||||||
|         - VERSION=${VERSION} |  | ||||||
|         - SRC_DIR=${SRC_DIR} |  | ||||||
|         - SCRIPTS_DIR=${SCRIPTS_DIR} |  | ||||||
|         - APP_DIR=${APP_DIR} |  | ||||||
|         - DATA_DIR=${DATA_DIR} |  | ||||||
|         - WEB_PORT=${WEB_PORT} |  | ||||||
|         - USER_NAME=${USER_NAME} |  | ||||||
|         - USER_GROUP=${USER_GROUP} |  | ||||||
|         - APP_UID=${APP_UID} |  | ||||||
|         - APP_GID=${APP_GID} |  | ||||||
|     ports: |  | ||||||
|       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" |  | ||||||
|     volumes: |  | ||||||
|       - "/opt/collector/data:${DATA_DIR}" |  | ||||||
|       - "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3" |  | ||||||
|     env_file: |  | ||||||
|       - /.env |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 🔴 |  | ||||||
| 
 |  | ||||||
| ❗IMPORTANT❗ |  | ||||||
| 
 |  | ||||||
| If you are using bind-mount and mounting it to your application's storage, |  | ||||||
| remember user in container is not privileged UID 1000 if mounted file |  | ||||||
| or the directory will belong to the root |  | ||||||
| application will not be able to read it and therefore work. |  | ||||||
| 
 |  | ||||||
| In a production environment, use the application behind your favorite reverse proxy. |  | ||||||
| 
 |  | ||||||
| Just add it to the docker-compose.yaml stack |  | ||||||
| 
 |  | ||||||
| >You don't have to do this, but Gunicorn recommends following this rule. |  | ||||||
| > |  | ||||||
| >I agree with them, so you have been warned) |  | ||||||
| 
 |  | ||||||
| 🔴 |  | ||||||
| 
 |  | ||||||
| ## Environment: |  | ||||||
| >The application can be configured, |  | ||||||
| >to do this, pass the following possible variables surroundings. |  | ||||||
| >If no variable is passed, the default environment variable will be used |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
|  █▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀ |  | ||||||
|  █▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| | ENV                  | DEFAULT         | INFO                     | |  | ||||||
| | -------------------- | --------------- | ------------------------ | |  | ||||||
| | SECRET_KEY           | j9QGbvM9Z4otb47 | ❗change this immediately| |  | ||||||
| | DEBUG                | False           | use only False in prod   | |  | ||||||
| | ALLOWED_HOSTS        | '*'             | list separated by commas | |  | ||||||
| | CSRF_TRUSTED_ORIGINS |                 | list separated by commas | |  | ||||||
| | DB_URL               |                 | url for connect db       | |  | ||||||
| | TZ                   | 'UTC'           | server timezone          | |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| [CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins) |  | ||||||
| 
 |  | ||||||
| Required in a Docker environment in a production environment |  | ||||||
| accepts a list of urls separated by commas |  | ||||||
| >http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| [DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html) |  | ||||||
| 
 |  | ||||||
| Must be specified if you want to use PostgreSQL |  | ||||||
| This data must match the PostgreSQL container variables |  | ||||||
| 
 |  | ||||||
| | ENV               | VALUE          | |  | ||||||
| | ----------------- | -------------- | |  | ||||||
| | POSTGRES_USER     | admin          | |  | ||||||
| | POSTGRES_PASSWORD | ddkwndkjdX7RrP | |  | ||||||
| | POSTGRES_DB       | collector      | |  | ||||||
| 
 |  | ||||||
| Example: |  | ||||||
| 
 |  | ||||||
| #### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector |  | ||||||
| - Protocol: **psql://** |  | ||||||
| - User: **admin** |  | ||||||
| - Password: **ddkwndkjdX7RrP** |  | ||||||
| - Address: **psql-collector** |  | ||||||
| - Port: **5432** |  | ||||||
| - Database name: **collector** |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| █▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀ |  | ||||||
| █▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄ |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| | ENV                         | DEFAULT        | |  | ||||||
| | --------------------------- | -------------- | |  | ||||||
| | GUNICORN_BIND               | '0.0.0.0:8000' | |  | ||||||
| | GUNICORN_BACKLOG            | 2048           | |  | ||||||
| | GUNICORN_WORKERS            | 2              | |  | ||||||
| | GUNICORN_WORKER_CLASS       | 'sync'         | |  | ||||||
| | GUNICORN_WORKER_CONNECTIONS | 1000           | |  | ||||||
| | GUNICORN_THREADS            | 1              | |  | ||||||
| | GUNICORN_TIMEOUT            | 3600           | |  | ||||||
| | GUNICORN_KEEPALIVE          | 2              | |  | ||||||
| | GUNICORN_LOGLEVEL           | 'info'         | |  | ||||||
| 
 |  | ||||||
| [GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html) |  | ||||||
| 
 |  | ||||||
| Detailed information about each environment variable is available in |  | ||||||
| the official documentation. |  | ||||||
| 
 |  | ||||||
| **GUNICORN_BIND** do not change this since the variable  |  | ||||||
| is responsible for the listening address and port inside the container. |  | ||||||
| 
 |  | ||||||
| **GUNICORN_TIMEOUT** is set to 3600 by default. |  | ||||||
| Such a large timeout is needed to download large files. |  | ||||||
| Since I tried to make the application minimalistic and not use a task manager, |  | ||||||
| the file is downloaded in one thread. |  | ||||||
| 
 |  | ||||||
| If the loading time is more than an hour, the connection will be broken, |  | ||||||
| this is a feature of the synchronous operation of gunicorn workers; |  | ||||||
| if you do not have enough time to load, you can increase this value. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ❗IMPORTANT❗ |  | ||||||
| 
 |  | ||||||
| Gunicorn is configured to write to the log in the following format: |  | ||||||
| ```python |  | ||||||
| '%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' |  | ||||||
| ``` |  | ||||||
| This means that the log will show the IP address of the request only from the header |  | ||||||
| 
 |  | ||||||
| **X-Forwarded-For** |  | ||||||
| 
 |  | ||||||
| In a production environment, the application must be behind a reverse proxy |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Helpers |  | ||||||
| At the root of the project repository there is a scripts directory, |  | ||||||
| it contains the uploader.sh script with which you can send files |  | ||||||
| from the console using **curl**. |  | ||||||
| 
 |  | ||||||
| The syntax is simple: |  | ||||||
| 
 |  | ||||||
| ```cmd |  | ||||||
| Usage: ./uploader.sh [options [parameters]] |  | ||||||
| 
 |  | ||||||
| Options: |  | ||||||
| 
 |  | ||||||
|  -f | --file     full path to upload file required |  | ||||||
|  -t | --token    access token             required |  | ||||||
|  -u | --url      target url               required |  | ||||||
|  -v | --version  print version |  | ||||||
|  -h | --help     print help |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## License |  | ||||||
| 
 |  | ||||||
| GNU GPL 3.0 |  | ||||||
| @ -1,5 +1,4 @@ | |||||||
| import os | import os | ||||||
| import logging |  | ||||||
| from django.core.management.base import BaseCommand | from django.core.management.base import BaseCommand | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| @ -7,31 +6,6 @@ from django.conf import settings | |||||||
| from django.db.models import FileField | from django.db.models import FileField | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) |  | ||||||
| 
 |  | ||||||
| logging.config.dictConfig({ |  | ||||||
|     'version': 1, |  | ||||||
|     'disable_existing_loggers': False, |  | ||||||
|     'formatters': { |  | ||||||
|         'console': { |  | ||||||
|             'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
|     'handlers': { |  | ||||||
|         'console': { |  | ||||||
|             'class': 'logging.StreamHandler', |  | ||||||
|             'formatter': 'console' |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
|     'loggers': { |  | ||||||
|         '': { |  | ||||||
|             'level': 'INFO', |  | ||||||
|             'handlers': ['console'] |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||||
|     # HELP MESSAGE: |     # HELP MESSAGE: | ||||||
|     help_part1 = 'This command deletes all media files from' |     help_part1 = 'This command deletes all media files from' | ||||||
| @ -40,12 +14,10 @@ class Command(BaseCommand): | |||||||
|     help = f'{help_part1} {help_part2} {help_part3}' |     help = f'{help_part1} {help_part2} {help_part3}' | ||||||
| 
 | 
 | ||||||
|     def handle(self, *args, **options): |     def handle(self, *args, **options): | ||||||
|         logger.info('Start cleanup storage....') |  | ||||||
|         all_models = apps.get_models() |         all_models = apps.get_models() | ||||||
|         physical_files = set() |         physical_files = set() | ||||||
|         db_files = set() |         db_files = set() | ||||||
|         # Get all files from the database |         # Get all files from the database | ||||||
|         logger.info('Get all files from the database....') |  | ||||||
|         for model in all_models: |         for model in all_models: | ||||||
|             file_fields = [] |             file_fields = [] | ||||||
|             filters = Q() |             filters = Q() | ||||||
| @ -63,9 +35,7 @@ class Command(BaseCommand): | |||||||
|                     flat=True |                     flat=True | ||||||
|                 ).distinct() |                 ).distinct() | ||||||
|                 db_files.update(files) |                 db_files.update(files) | ||||||
|         logger.info(f'Find: {len(db_files)} files from the database') |  | ||||||
|         # Get all files from the MEDIA_ROOT, recursively |         # Get all files from the MEDIA_ROOT, recursively | ||||||
|         logger.info('Get all files from the MEDIA_ROOT, recursively....') |  | ||||||
|         media_root = getattr(settings, 'MEDIA_ROOT', None) |         media_root = getattr(settings, 'MEDIA_ROOT', None) | ||||||
|         if media_root is not None: |         if media_root is not None: | ||||||
|             for relative_root, dirs, files in os.walk(media_root): |             for relative_root, dirs, files in os.walk(media_root): | ||||||
| @ -76,21 +46,14 @@ class Command(BaseCommand): | |||||||
|                         os.path.relpath(relative_root, media_root), file_ |                         os.path.relpath(relative_root, media_root), file_ | ||||||
|                     ) |                     ) | ||||||
|                     physical_files.add(relative_file) |                     physical_files.add(relative_file) | ||||||
|         logger.info(f'Find: {len(physical_files)} files from the MEDIA_ROOT') |  | ||||||
|         # Compute the difference and delete those files |         # Compute the difference and delete those files | ||||||
|         logger.info('Compute the difference and delete those files....') |  | ||||||
|         deletables = physical_files - db_files |         deletables = physical_files - db_files | ||||||
|         logger.info(f'Find: {len(deletables)} orphan files') |  | ||||||
|         if deletables: |         if deletables: | ||||||
|             for file_ in deletables: |             for file_ in deletables: | ||||||
|                 logger.info(f"Delete orphan file: {file_}") |  | ||||||
|                 os.remove(os.path.join(media_root, file_)) |                 os.remove(os.path.join(media_root, file_)) | ||||||
|             # Bottom-up - delete all empty folders |             # Bottom-up - delete all empty folders | ||||||
|             logger.info('Bottom-up - delete all empty folders....') |  | ||||||
|             for relative_root, dirs, files in os.walk( |             for relative_root, dirs, files in os.walk( | ||||||
|                     media_root, topdown=False): |                     media_root, topdown=False): | ||||||
|                 for dir_ in dirs: |                 for dir_ in dirs: | ||||||
|                     if not os.listdir(os.path.join(relative_root, dir_)): |                     if not os.listdir(os.path.join(relative_root, dir_)): | ||||||
|                         os.rmdir(os.path.join(relative_root, dir_)) |                         os.rmdir(os.path.join(relative_root, dir_)) | ||||||
|             logger.info('Done! Storage has been cleaned up') |  | ||||||
|         logger.info('Done! Nothing to delete') |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user