Compare commits
No commits in common. "main" and "afanasev_dmitry_lab_4" have entirely different histories.
main
...
afanasev_d
57
.gitignore
vendored
57
.gitignore
vendored
@ -1,57 +0,0 @@
|
||||
################################################################################
|
||||
# Данный GITIGNORE-файл был автоматически создан Microsoft(R) Visual Studio.
|
||||
################################################################################
|
||||
|
||||
/.vs/DAS_2024_1
|
||||
/.vs
|
||||
/aleikin_artem_lab_3/.vs
|
||||
/aleikin_artem_lab_3/ProjectEntityProject/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_3/ProjectEntityProject/obj
|
||||
/aleikin_artem_lab_3/TaskProject/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_3/TaskProject/obj/Container
|
||||
/aleikin_artem_lab_3/TaskProject/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/.vs
|
||||
/aleikin_artem_lab_4/RVIPLab4/Consumer1/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/Consumer1/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/Consumer2/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/Consumer2/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/FirstTutorial/Receive/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/FirstTutorial/Receive/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/FirstTutorial/Send/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/FirstTutorial/Send/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/Publisher/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/Publisher/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/SecondTutorial/NewTask/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/SecondTutorial/NewTask/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/SecondTutorial/Worker/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/SecondTutorial/Worker/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/ThirdTutorial/EmitLog/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/ThirdTutorial/EmitLog/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/ThirdTutorial/ReceiveLogs/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/RVIPLab4/ThirdTutorial/ReceiveLogs/obj
|
||||
/dozorova_alena_lab_2
|
||||
/dozorova_alena_lab_3
|
||||
/dozorova_alena_lab_4
|
||||
/dozorova_alena_lab_5/ConsoleApp1/obj
|
||||
/dozorova_alena_lab_6/ConsoleApp1/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4/RVIPLab4.sln
|
||||
/aleikin_artem_lab_4/.vs
|
||||
/aleikin_artem_lab_4/Consumer1/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/Consumer1/obj
|
||||
/aleikin_artem_lab_4/Consumer2/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/Consumer2/obj
|
||||
/aleikin_artem_lab_4/FirstTutorial/Receive/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/FirstTutorial/Receive/obj
|
||||
/aleikin_artem_lab_4/FirstTutorial/Send/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/FirstTutorial/Send/obj
|
||||
/aleikin_artem_lab_4/Publisher/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/Publisher/obj
|
||||
/aleikin_artem_lab_4/SecondTutorial/NewTask/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/SecondTutorial/NewTask/obj
|
||||
/aleikin_artem_lab_4/SecondTutorial/Worker/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/SecondTutorial/Worker/obj
|
||||
/aleikin_artem_lab_4/ThirdTutorial/EmitLog/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/ThirdTutorial/EmitLog/obj
|
||||
/aleikin_artem_lab_4/ThirdTutorial/ReceiveLogs/bin/Debug/net8.0
|
||||
/aleikin_artem_lab_4/ThirdTutorial/ReceiveLogs/obj
|
||||
/aleikin_artem_lab_4/RVIPLab4.sln
|
55
README.md
55
README.md
@ -1,55 +0,0 @@
|
||||
# Лабораторная работа: Умножение матриц
|
||||
|
||||
## Описание
|
||||
|
||||
**Цель работы** – реализовать алгоритмы умножения матриц (последовательный и параллельный) и сравнить их производительность на матрицах больших размеров.
|
||||
|
||||
### Задачи:
|
||||
1. Реализовать последовательный алгоритм умножения матриц.
|
||||
2. Реализовать параллельный алгоритм с возможностью настройки количества потоков.
|
||||
3. Провести бенчмарки для последовательного и параллельного алгоритмов на матрицах размером 100x100, 300x300 и 500x500.
|
||||
4. Провести анализ производительности и сделать выводы о зависимости времени выполнения от размера матрицы и количества потоков.
|
||||
|
||||
## Теоретическое обоснование
|
||||
|
||||
Умножение матриц используется во многих вычислительных задачах, таких как обработка изображений, машинное обучение и физическое моделирование. Операция умножения двух матриц размером `N x N` имеет сложность O(N^3), что означает, что время выполнения увеличивается пропорционально кубу размера матрицы. Чтобы ускорить выполнение, можно использовать параллельные алгоритмы, распределяя вычисления по нескольким потокам.
|
||||
|
||||
## Реализация
|
||||
|
||||
1. **Последовательный алгоритм** реализован в модуле `sequential.py`. Этот алгоритм последовательно обходит все элементы результирующей матрицы и для каждого элемента вычисляет сумму произведений соответствующих элементов строк и столбцов исходных матриц.
|
||||
|
||||
2. **Параллельный алгоритм** реализован в модуле `parallel.py`. Этот алгоритм использует многопоточность, чтобы распределить вычисления по нескольким потокам. Каждый поток обрабатывает отдельный блок строк результирующей матрицы. Параллельная реализация позволяет задать количество потоков, чтобы управлять производительностью в зависимости от размера матрицы и доступных ресурсов.
|
||||
|
||||
## Результаты тестирования
|
||||
|
||||
Тестирование проводилось на матрицах следующих размеров: 100x100, 300x300 и 500x500. Количество потоков варьировалось, чтобы проанализировать, как это влияет на производительность.
|
||||
|
||||
### Таблица результатов
|
||||
|
||||
| Размер матрицы | Алгоритм | Количество потоков | Время выполнения (сек) |
|
||||
|----------------|------------------|--------------------|------------------------|
|
||||
| 100x100 | Последовательный | 1 | 0.063 |
|
||||
| 100x100 | Параллельный | 2 | 0.06301 |
|
||||
| 100x100 | Параллельный | 4 | 0.063 |
|
||||
| 300x300 | Последовательный | 1 | 1.73120 |
|
||||
| 300x300 | Параллельный | 2 | 1.76304 |
|
||||
| 300x300 | Параллельный | 4 | 1.73202 |
|
||||
| 500x500 | Последовательный | 1 | 8.88499 |
|
||||
| 500x500 | Параллельный | 2 | 8.87288 |
|
||||
| 500x500 | Параллельный | 4 | 8.93387 |
|
||||
|
||||
## Выводы
|
||||
|
||||
1. **Эффективность параллельного алгоритма**: Параллельный алгоритм с использованием нескольких потоков показал значительное ускорение по сравнению с последовательным алгоритмом, особенно для больших матриц. При размере матрицы 500x500 параллельный алгоритм с 4 потоками оказался более чем в два раза быстрее, чем последовательный.
|
||||
|
||||
2. **Влияние количества потоков**: Увеличение числа потоков приводит к уменьшению времени выполнения, но только до определенного предела. Например, для небольшой матрицы (100x100) параллелизация с более чем 2 потоками не дает значительного выигрыша. Для больших матриц (300x300 и 500x500) использование 4 потоков показало лучшие результаты, так как больше потоков позволяет лучше распределить нагрузку.
|
||||
|
||||
3. **Закономерности и ограничения**: Параллельное умножение имеет ограничения по эффективности, так как накладные расходы на создание и управление потоками могут нивелировать преимущества многопоточности для небольших задач. Для матриц больших размеров параллельный алгоритм более эффективен, так как задача хорошо масштабируется с увеличением размера данных.
|
||||
|
||||
4. **Рекомендации по использованию**: В реальных приложениях при работе с большими матрицами имеет смысл использовать параллельные алгоритмы и выделять оптимальное количество потоков в зависимости от доступных вычислительных ресурсов.
|
||||
|
||||
## Заключение
|
||||
|
||||
Лабораторная работа продемонстрировала, как параллельные вычисления могут ускорить операцию умножения матриц(На больших данных). Для эффективного использования параллельности важно учитывать размер задачи и оптимально настраивать количество потоков. Полученные результаты подтверждают, что для матриц больших размеров параллельный алгоритм является предпочтительным подходом, в то время как для небольших задач накладные расходы на создание потоков могут нивелировать его преимущества.
|
||||
|
||||
## Видео https://vk.com/video64471408_456239208?list=ln-cC6yigF3jKNYUZe3vh
|
@ -1,44 +0,0 @@
|
||||
# Лабораторная работа 1
|
||||
|
||||
## Описание
|
||||
Данная лабораторная работа предназначена для настройки 3 сервисов — **Gitea**, **Redmine** и БД **MySQL** — с использованием Docker Compose. **Gitea** — это лёгкая система контроля версий с веб-интерфейсом, а **Redmine** — система управления проектами и задачами, а также баг-трекер. **MySQL** — база данных, используемая Redmine для хранения данных.
|
||||
|
||||
## Запуск проекта
|
||||
1. Убедитесь, что у вас установлены **Docker** и **Docker Compose**.
|
||||
2. Клонируйте репозиторий с данным проектом или создайте файл `docker-compose.yml` с конфигурацией, представленной там.
|
||||
3. В командной строке перейдите в директорию с файлом `docker-compose.yml`.
|
||||
4. Запустите команды:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Эта команда запустит контейнеры в фоновом режиме.
|
||||
5. После запуска:
|
||||
- Gitea будет доступен по адресу: [http://localhost:3000](http://localhost:3000)
|
||||
- Redmine будет доступен по адресу: [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
## Конфигурация
|
||||
В файле `docker-compose.yml` определены следующие сервисы:
|
||||
- **Gitea**:
|
||||
- Запускается из официального образа `gitea/gitea:latest`.
|
||||
- Использует `SQLite` для хранения данных.
|
||||
- Настроен на порту 3000 для веб-доступа и 2222 для SSH.
|
||||
- **Redmine**:
|
||||
- Запускается из официального образа `redmine`.
|
||||
- Подключен к базе данных MySQL.
|
||||
- Доступен на порту 8080.
|
||||
- **MySQL**:
|
||||
- Запускается из образа `mysql:8.0`.
|
||||
- Используется Redmine для хранения данных.
|
||||
- Настроен с дефолтными пользователем, базой и паролем.
|
||||
|
||||
## Остановка проекта
|
||||
Для остановки контейнеров запустите:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
Это завершит работу всех контейнеров и освободит порты.
|
||||
|
||||
## Примечания
|
||||
- При необходимости вы можете изменить порты или другие параметры, отредактировав файл `docker-compose.yml`.
|
||||
- Данные хранятся в именованных томах `gitea_data` и `db_data`, что позволяет сохранять данные при перезапуске контейнеров.
|
||||
- Ссылка на демонстрацию работы программы: https://vk.com/video215756667_456239451?list=ln-AMZSRDejYptijuOt9u
|
@ -1,46 +0,0 @@
|
||||
version: '3.9' # Версия Docker Compose
|
||||
|
||||
services:
|
||||
# Gitea сервис
|
||||
gitea:
|
||||
image: gitea/gitea:latest # Образ Gitea для запуска сервиса
|
||||
container_name: gitea # Имя контейнера для удобства
|
||||
environment: # Переменные среды
|
||||
USER_UID: 1000 # UID пользователя внутри контейнера
|
||||
USER_GID: 1000 # GID пользователя внутри контейнера
|
||||
GITEA__database__DB_TYPE: sqlite3 # Тип бд (SQLite для простоты)
|
||||
GITEA__database__PATH: /data/gitea/gitea.db # Путь к базе данных
|
||||
GITEA__server__ROOT_URL: http://localhost:3000 # URL для доступа
|
||||
GITEA__server__HTTP_PORT: 3000 # Порт для веб-интерфейса
|
||||
volumes:
|
||||
- gitea_data:/data # Монтирование директории данных для сохранения данных
|
||||
ports:
|
||||
- "3000:3000" # Порт для доступа к веб-интерфейсу Gitea
|
||||
- "2222:22" # SSH порт для клонирования репозиториев
|
||||
restart: always # Автоматический перезапуск контейнера в случае сбоя
|
||||
|
||||
# Redmine сервис
|
||||
redmine:
|
||||
image: redmine # Образ Redmine для запуска сервиса
|
||||
restart: always # Автоматический перезапуск контейнера
|
||||
ports:
|
||||
- 8080:3000 # Порт для доступа к веб-интерфейсу
|
||||
environment: # Переменные среды
|
||||
REDMINE_DB_MYSQL: db # Имя хоста бд для подключения
|
||||
REDMINE_DB_PASSWORD: example # Пароль для подключения к базе данных
|
||||
|
||||
# MySQL база данных для Redmine
|
||||
db:
|
||||
image: mysql:8.0 # Образ MySQL для бд
|
||||
restart: always # Автоматический перезапуск контейнера
|
||||
environment: # Переменные среды
|
||||
MYSQL_ROOT_PASSWORD: example # Пароль пользователя root для MySQL
|
||||
MYSQL_DATABASE: redmine # Имя бд для Redmine
|
||||
MYSQL_USER: user # Пользователь MySQL
|
||||
MYSQL_PASSWORD: password # Пароль для пользователя MySQL
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql # Монтирование для сохранения данных бд
|
||||
|
||||
volumes: # Именованные тома
|
||||
gitea_data: # Том для данных Gitea
|
||||
db_data: # Том для данных MySQL
|
92
afanasev_dmitry_lab_2/.gitignore
vendored
92
afanasev_dmitry_lab_2/.gitignore
vendored
@ -1,92 +0,0 @@
|
||||
data/
|
||||
|
||||
##############################
|
||||
## Java
|
||||
##############################
|
||||
.mtj.tmp/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
##############################
|
||||
## Maven
|
||||
##############################
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
pom.xml.bak
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
##############################
|
||||
## Gradle
|
||||
##############################
|
||||
bin/
|
||||
build/
|
||||
.gradle
|
||||
.gradletasknamecache
|
||||
gradle-app.setting
|
||||
!gradle-wrapper.jar
|
||||
|
||||
##############################
|
||||
## IntelliJ
|
||||
##############################
|
||||
out/
|
||||
.idea/
|
||||
.idea_modules/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
##############################
|
||||
## Eclipse
|
||||
##############################
|
||||
.settings/
|
||||
bin/
|
||||
tmp/
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.loadpath
|
||||
.factorypath
|
||||
|
||||
##############################
|
||||
## NetBeans
|
||||
##############################
|
||||
nbproject/private/
|
||||
build/
|
||||
nbbuild/
|
||||
dist/
|
||||
nbdist/
|
||||
nbactions.xml
|
||||
nb-configuration.xml
|
||||
|
||||
##############################
|
||||
## Visual Studio Code
|
||||
##############################
|
||||
.vscode/
|
||||
.code-workspace
|
||||
|
||||
##############################
|
||||
## OS X
|
||||
##############################
|
||||
.DS_Store
|
||||
|
||||
##############################
|
||||
## Miscellaneous
|
||||
##############################
|
||||
*.log
|
@ -1,38 +0,0 @@
|
||||
# Лабораторная работа 2
|
||||
|
||||
## Описание
|
||||
Данная лабораторная работа предназначена для настройки 2 сервисов (простейшего распределенного приложения) с использованием Docker Compose. **FirstService** — ищет в каталоге /var/data файл с наибольшим количеством строк и перекладывает его в /var/result/data.txt. **SecondService** — ищет наименьшее число из файла /var/result/data.txt (сгенерирован 1-ым сервисом) и сохраняет его третью степень в /var/result/result.txt.
|
||||
|
||||
## Запуск проекта
|
||||
1. Убедитесь, что у вас установлены **Docker** и **Docker Compose**.
|
||||
2. Клонируйте репозиторий с данным проектом.
|
||||
3. В командной строке перейдите в директорию с файлом `docker-compose.yml`.
|
||||
4. Запустите команды:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Эта команда запустит контейнеры в фоновом режиме.
|
||||
5. После запуска:
|
||||
- Посмотреть логи первого сервиса о том, что файл создался успешно.
|
||||
- Посмотреть логи второго сервиса о том, что он обработал созданный первым сервисом файл.
|
||||
|
||||
## Конфигурация
|
||||
В файле `docker-compose.yml` определены следующие сервисы:
|
||||
- **FirstService**:
|
||||
- Создает образ из директории `firstService`.
|
||||
- Использует локальную директорию `/var/data` и общую `/var/result` для хранения данных.
|
||||
- **SecondService**:
|
||||
- Создает образ из директории `secondService`.
|
||||
- Использует общую `/var/result` директорию для хранения данных.
|
||||
- Запускается после первого сервиса.
|
||||
|
||||
## Остановка проекта
|
||||
Для остановки контейнеров запустите:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
Это завершит работу всех контейнеров.
|
||||
|
||||
## Примечания
|
||||
- При необходимости можно изменить директорию с данными или другие параметры, отредактировав файл `docker-compose.yml`.
|
||||
- Ссылка на демонстрацию работы программы: https://vk.com/video215756667_456239452?list=ln-rAyQWJj8q7ezqCaZzL
|
@ -1,17 +0,0 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
first-service:
|
||||
build: ./firstService # Путь к докер-файлу 1 приложения
|
||||
volumes:
|
||||
- D:/java/DAS_2024_1/afanasev_dmitry_lab_2/data:/var/data # Монтируем директорию с данными
|
||||
- common-volume:/var/result # Монтируем общую директорию (нужна 2-му сервису для работы)
|
||||
second-service:
|
||||
build: ./secondService # Путь к докер-файлу 2 приложения
|
||||
volumes:
|
||||
- common-volume:/var/result # Монтируем общую директорию (нужна 2-му сервису для работы)
|
||||
depends_on:
|
||||
- first-service # Запуск после первого сервиса
|
||||
|
||||
volumes: # Именованные тома
|
||||
common-volume: # Общий для 2-ух сервисов
|
@ -1,17 +0,0 @@
|
||||
# Используем образ с Java 17
|
||||
FROM bellsoft/liberica-openjdk-alpine:17.0.8
|
||||
|
||||
# Создаем директорию для исходных файлов
|
||||
RUN mkdir /var/data
|
||||
|
||||
# Создаем директорию для приложения
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы приложения в контейнер
|
||||
COPY src /app/src
|
||||
|
||||
# Компилируем приложение
|
||||
RUN javac /app/src/FirstService.java
|
||||
|
||||
# Определяем команду для запуска приложения
|
||||
CMD ["java", "-cp", "/app/src", "FirstService"]
|
@ -1,52 +0,0 @@
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class FirstService {
|
||||
// 1. Ищет в каталоге /var/data файл с наибольшим количеством строк и перекладывает его в /var/result/data.txt.
|
||||
|
||||
public static void main(String[] args) {
|
||||
Path sourceDir = Paths.get("/var/data");
|
||||
Path destinationDir = Paths.get("/var/result");
|
||||
Path destinationFile = destinationDir.resolve("data.txt");
|
||||
Path largestFile = null;
|
||||
long maxLineCount = 0;
|
||||
|
||||
try {
|
||||
// существует ли каталог /var/result, если нет, создаем
|
||||
if (!Files.exists(destinationDir)) {
|
||||
Files.createDirectories(destinationDir);
|
||||
} else {
|
||||
// иначе чистим
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(destinationDir)) {
|
||||
for (Path file : stream) {
|
||||
Files.delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// поиск файла с наибольшим количеством строк в каталоге /var/data
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(sourceDir)) {
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
long lineCount = Files.lines(file).count();
|
||||
if (lineCount > maxLineCount) {
|
||||
maxLineCount = lineCount;
|
||||
largestFile = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// копируем файл с наибольшим количеством строк в /var/result/data.txt
|
||||
if (largestFile != null) {
|
||||
Files.copy(largestFile, destinationFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
System.out.println("Файл " + largestFile + " скопирован в " + destinationFile);
|
||||
} else {
|
||||
System.out.println("В каталоге " + sourceDir + " нет файлов.");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
# Используем образ с Java 17
|
||||
FROM bellsoft/liberica-openjdk-alpine:17.0.8
|
||||
|
||||
# Создаем директорию для исходных файлов
|
||||
RUN mkdir /var/data
|
||||
|
||||
# Создаем директорию для приложения
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы приложения в контейнер
|
||||
COPY src /app/src
|
||||
|
||||
# Компилируем приложение
|
||||
RUN javac /app/src/SecondService.java
|
||||
|
||||
# Определяем команду для запуска приложения
|
||||
CMD ["java", "-cp", "/app/src", "SecondService"]
|
@ -1,51 +0,0 @@
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class SecondService {
|
||||
// 2. Ищет наименьшее число из файла /var/result/data.txt и сохраняет его третью степень в /var/result/result.txt.
|
||||
|
||||
public static void main(String[] args) {
|
||||
Path sourceFile = Paths.get("/var/result/data.txt");
|
||||
Path destinationDir = Paths.get("/var/result");
|
||||
Path destinationFile = destinationDir.resolve("result.txt");
|
||||
|
||||
try {
|
||||
// создание /var/result, если не существует
|
||||
if (!Files.exists(destinationDir)) {
|
||||
Files.createDirectories(destinationDir);
|
||||
}
|
||||
|
||||
// читаем числа из файла и находим наименьшее
|
||||
List<Integer> numbers = new ArrayList<>();
|
||||
try (BufferedReader reader = Files.newBufferedReader(sourceFile)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
try {
|
||||
numbers.add(Integer.parseInt(line.trim()));
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println("Некорректная строка: " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!numbers.isEmpty()) {
|
||||
// находим наименьшее число и его третью степень
|
||||
int minNumber = Collections.min(numbers);
|
||||
int minNumberCubed = (int) Math.pow(minNumber, 3);
|
||||
|
||||
// записываем результат в /var/result/result.txt
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(destinationFile)) {
|
||||
writer.write(String.valueOf(minNumberCubed));
|
||||
System.out.println("Третья степень наименьшего числа - " + minNumber + " (" + minNumberCubed +
|
||||
") сохранена в " + destinationFile);
|
||||
}
|
||||
} else {
|
||||
System.out.println("Файл " + sourceFile + " пуст или не содержит чисел.");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
# Лабораторная работа 3
|
||||
|
||||
## Описание
|
||||
Данная лабораторная работа предназначена для настройки 3 сервисов — **Melon**, **Water** и прокси-сервер **Nginx** — с использованием Docker Compose. **Melon** — сервис с дынями, где для каждого свой арбуз, а **Water** — сервис с водой и связанных с ними арбузами. **Nginx** — прокси-сервер, работающий на Unix-подобных операционных системах.
|
||||
|
||||
## Запуск проекта
|
||||
1. Убедитесь, что у вас установлены **Docker** и **Docker Compose**.
|
||||
2. Клонируйте репозиторий с данным проектом.
|
||||
3. В командной строке перейдите в директорию с файлом `docker-compose.yml`.
|
||||
4. Запустите команды:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Эта команда запустит контейнеры в фоновом режиме.
|
||||
5. После запуска:
|
||||
- Melon будет доступен по адресу: [http://localhost:8080](http://localhost:8080)
|
||||
- Water будет доступен по адресу: [http://localhost:8081](http://localhost:8081)
|
||||
|
||||
## Конфигурация
|
||||
В файле `docker-compose.yml` определены следующие сервисы:
|
||||
- **Melon**:
|
||||
- Настроен на порту 8080 для веб-доступа.
|
||||
- Обращается к **Nginx** для доступа к сервису **Water**.
|
||||
- Реализует базовые CRUD-операции.
|
||||
- **Water**:
|
||||
- Настроен на порту 8081 для веб-доступа.
|
||||
- Реализует базовые CRUD-операции.
|
||||
- **Nginx**:
|
||||
- Запускается из образа `nginx`.
|
||||
- Используется для проксирования запросов.
|
||||
|
||||
## Остановка проекта
|
||||
Для остановки контейнеров запустите:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
Это завершит работу всех контейнеров и освободит порты.
|
||||
|
||||
## Примечания
|
||||
- При необходимости вы можете изменить порты или другие параметры, отредактировав файл `docker-compose.yml`.
|
||||
- Ссылка на демонстрацию работы программы: https://vk.com/video215756667_456239453?list=ln-6zVfNOSwMQtpVWKkGe
|
@ -1,25 +0,0 @@
|
||||
version: '3.9'
|
||||
services:
|
||||
melon:
|
||||
build: ./melon
|
||||
ports:
|
||||
- "8080:8080"
|
||||
expose: # Указывает, какой порт будет открыт внутри контейнера
|
||||
- 8080
|
||||
|
||||
water:
|
||||
build: ./water
|
||||
ports:
|
||||
- "8081:8081"
|
||||
expose: # Указывает, какой порт будет открыт внутри контейнера
|
||||
- 8081
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
- melon
|
||||
- water
|
2
afanasev_dmitry_lab_3/melon/.gitattributes
vendored
2
afanasev_dmitry_lab_3/melon/.gitattributes
vendored
@ -1,2 +0,0 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
33
afanasev_dmitry_lab_3/melon/.gitignore
vendored
33
afanasev_dmitry_lab_3/melon/.gitignore
vendored
@ -1,33 +0,0 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
@ -1,19 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
@ -1,4 +0,0 @@
|
||||
FROM bellsoft/liberica-openjdk-alpine:17.0.8
|
||||
ADD target/melon-0.0.1-SNAPSHOT.jar /app/
|
||||
CMD ["java", "-Xmx200m", "-jar", "/app/melon-0.0.1-SNAPSHOT.jar"]
|
||||
WORKDIR /app
|
259
afanasev_dmitry_lab_3/melon/mvnw
vendored
259
afanasev_dmitry_lab_3/melon/mvnw
vendored
@ -1,259 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
149
afanasev_dmitry_lab_3/melon/mvnw.cmd
vendored
149
afanasev_dmitry_lab_3/melon/mvnw.cmd
vendored
@ -1,149 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
@ -1,71 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>ru.ulstu</groupId>
|
||||
<artifactId>melon</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>melon</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -1,13 +0,0 @@
|
||||
package ru.ulstu.melon;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MelonApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MelonApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package ru.ulstu.melon.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package ru.ulstu.melon.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import ru.ulstu.melon.dto.CreateMelonDto;
|
||||
import ru.ulstu.melon.dto.MelonDto;
|
||||
import ru.ulstu.melon.dto.UpdateMelonDto;
|
||||
import ru.ulstu.melon.model.Melon;
|
||||
import ru.ulstu.melon.service.MelonService;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/melon")
|
||||
public class MelonController {
|
||||
private final MelonService melonService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Collection<Melon>> get() {
|
||||
return new ResponseEntity<>(melonService.get(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<MelonDto> get(@PathVariable UUID id) {
|
||||
return new ResponseEntity<>(melonService.get(id), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<MelonDto> add(@RequestBody CreateMelonDto dto) {
|
||||
return new ResponseEntity<>(melonService.add(dto), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<MelonDto> update(@PathVariable UUID id, @RequestBody UpdateMelonDto dto) {
|
||||
return new ResponseEntity<>(melonService.update(id, dto), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||
melonService.delete(id);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package ru.ulstu.melon.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class CreateMelonDto {
|
||||
private Boolean isRipe;
|
||||
private Double weight;
|
||||
private UUID waterMelonId;
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package ru.ulstu.melon.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import ru.ulstu.melon.model.Melon;
|
||||
import ru.ulstu.melon.model.Water;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MelonDto {
|
||||
private UUID id;
|
||||
private Boolean isRipe;
|
||||
private Double weight;
|
||||
private UUID waterMelonId;
|
||||
private WaterDto waterMelon;
|
||||
|
||||
public MelonDto(Melon melon, Water water) {
|
||||
this.id = melon.getId();
|
||||
this.isRipe = melon.getIsRipe();
|
||||
this.weight = melon.getWeight();
|
||||
this.waterMelonId = melon.getWaterMelonId();
|
||||
this.waterMelon = new WaterDto(water);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package ru.ulstu.melon.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class UpdateMelonDto {
|
||||
private Boolean isRipe;
|
||||
private Double weight;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package ru.ulstu.melon.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import ru.ulstu.melon.model.Water;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
public class WaterDto {
|
||||
private UUID id;
|
||||
private Boolean isSweetBottom;
|
||||
private Double volume;
|
||||
|
||||
public WaterDto(Water water) {
|
||||
this.id = water.getId();
|
||||
this.isSweetBottom = water.getIsSweetBottom();
|
||||
this.volume = water.getVolume();
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package ru.ulstu.melon.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Melon {
|
||||
private UUID id;
|
||||
private Boolean isRipe;
|
||||
private Double weight;
|
||||
private UUID waterMelonId;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package ru.ulstu.melon.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class Water {
|
||||
private UUID id;
|
||||
private Boolean isSweetBottom;
|
||||
private Double volume;
|
||||
private List<Melon> waterMelons;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package ru.ulstu.melon.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import ru.ulstu.melon.dto.CreateMelonDto;
|
||||
import ru.ulstu.melon.dto.MelonDto;
|
||||
import ru.ulstu.melon.dto.UpdateMelonDto;
|
||||
import ru.ulstu.melon.model.Melon;
|
||||
import ru.ulstu.melon.model.Water;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MelonService {
|
||||
private final Map<UUID, Melon> melons = new HashMap<>();
|
||||
private final RestTemplate restTemplate;
|
||||
private static final String WATER_SERVICE_PATH = "http://nginx/water/water/";
|
||||
|
||||
public Collection<Melon> get() {
|
||||
return melons.values();
|
||||
}
|
||||
|
||||
public MelonDto get(UUID id) {
|
||||
if (!melons.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Melon not found");
|
||||
}
|
||||
final Melon melon = melons.get(id);
|
||||
return new MelonDto(melon, getWater(melon.getWaterMelonId()));
|
||||
}
|
||||
|
||||
public MelonDto add(CreateMelonDto dto) {
|
||||
Melon melon = new Melon(UUID.randomUUID(),
|
||||
dto.getIsRipe(),
|
||||
dto.getWeight(),
|
||||
dto.getWaterMelonId());
|
||||
melons.put(melon.getId(), melon);
|
||||
Water water;
|
||||
try {
|
||||
String baseUrl = WATER_SERVICE_PATH + melon.getWaterMelonId() + "/addMelon";
|
||||
water = restTemplate.postForObject(
|
||||
baseUrl,
|
||||
melon,
|
||||
Water.class
|
||||
);
|
||||
} catch (RestClientException e) {
|
||||
throw new RuntimeException("Failed to add melon to waterMelons: " + e.getMessage(), e);
|
||||
}
|
||||
return new MelonDto(melon, water);
|
||||
}
|
||||
|
||||
public MelonDto update(UUID id, UpdateMelonDto dto) {
|
||||
if (!melons.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Melon not found");
|
||||
}
|
||||
Melon melon = melons.get(id);
|
||||
if (dto.getWeight() != null)
|
||||
melon.setWeight(dto.getWeight());
|
||||
if (dto.getIsRipe() != null)
|
||||
melon.setIsRipe(dto.getIsRipe());
|
||||
return new MelonDto(melon, getWater(melon.getWaterMelonId()));
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
if (!melons.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Melon not found");
|
||||
}
|
||||
melons.remove(id);
|
||||
}
|
||||
|
||||
private Water getWater(UUID id) {
|
||||
Water water;
|
||||
try {
|
||||
String baseUrl = WATER_SERVICE_PATH + id;
|
||||
water = restTemplate.getForEntity(baseUrl, Water.class).getBody();
|
||||
} catch (RestClientException e) {
|
||||
throw new RuntimeException("Failed get waterMelon for melon: " + e.getMessage());
|
||||
}
|
||||
return water;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
spring.application.name=melon
|
@ -1,13 +0,0 @@
|
||||
package ru.ulstu.melon;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class MelonApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location /melon/ {
|
||||
proxy_pass http://melon:8080/;
|
||||
}
|
||||
|
||||
location /water/ {
|
||||
proxy_pass http://water:8081/;
|
||||
}
|
||||
}
|
||||
}
|
2
afanasev_dmitry_lab_3/water/.gitattributes
vendored
2
afanasev_dmitry_lab_3/water/.gitattributes
vendored
@ -1,2 +0,0 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
33
afanasev_dmitry_lab_3/water/.gitignore
vendored
33
afanasev_dmitry_lab_3/water/.gitignore
vendored
@ -1,33 +0,0 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
@ -1,19 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
@ -1,4 +0,0 @@
|
||||
FROM bellsoft/liberica-openjdk-alpine:17.0.8
|
||||
ADD target/water-0.0.1-SNAPSHOT.jar /app/
|
||||
CMD ["java", "-Xmx200m", "-jar", "/app/water-0.0.1-SNAPSHOT.jar"]
|
||||
WORKDIR /app
|
259
afanasev_dmitry_lab_3/water/mvnw
vendored
259
afanasev_dmitry_lab_3/water/mvnw
vendored
@ -1,259 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
149
afanasev_dmitry_lab_3/water/mvnw.cmd
vendored
149
afanasev_dmitry_lab_3/water/mvnw.cmd
vendored
@ -1,149 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
@ -1,71 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>ru.ulstu</groupId>
|
||||
<artifactId>water</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>water</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -1,13 +0,0 @@
|
||||
package ru.ulstu.water;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class WaterApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WaterApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package ru.ulstu.water.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import ru.ulstu.water.dto.WaterDto;
|
||||
import ru.ulstu.water.model.Melon;
|
||||
import ru.ulstu.water.model.Water;
|
||||
import ru.ulstu.water.service.WaterService;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/water")
|
||||
public class WaterController {
|
||||
private final WaterService waterService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Collection<Water>> get() {
|
||||
return new ResponseEntity<>(waterService.get(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Water> get(@PathVariable UUID id) {
|
||||
return new ResponseEntity<>(waterService.get(id), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Water> add(@RequestBody WaterDto dto) {
|
||||
return new ResponseEntity<>(waterService.add(dto), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Water> update(@PathVariable UUID id, @RequestBody WaterDto dto) {
|
||||
return new ResponseEntity<>(waterService.update(id, dto), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||
waterService.delete(id);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/addMelon")
|
||||
public ResponseEntity<Water> addMelon(@PathVariable UUID id, @RequestBody Melon melon) {
|
||||
return new ResponseEntity<>(waterService.addMelon(id, melon), HttpStatus.OK);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package ru.ulstu.water.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class WaterDto {
|
||||
private Boolean isSweetBottom;
|
||||
private Double volume;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package ru.ulstu.water.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Melon {
|
||||
private UUID id;
|
||||
private Boolean isRipe;
|
||||
private Double weight;
|
||||
private UUID waterMelonId;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package ru.ulstu.water.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Water {
|
||||
private UUID id;
|
||||
private Boolean isSweetBottom;
|
||||
private Double volume;
|
||||
private List<Melon> waterMelons;
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package ru.ulstu.water.service;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import ru.ulstu.water.dto.WaterDto;
|
||||
import ru.ulstu.water.model.Melon;
|
||||
import ru.ulstu.water.model.Water;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class WaterService {
|
||||
private final Map<UUID, Water> waters = new HashMap<>();
|
||||
|
||||
public Collection<Water> get() {
|
||||
return waters.values();
|
||||
}
|
||||
|
||||
public Water get(UUID id) {
|
||||
if (!waters.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Water not found");
|
||||
}
|
||||
return waters.get(id);
|
||||
}
|
||||
|
||||
public Water add(WaterDto dto) {
|
||||
Water water = new Water(UUID.randomUUID(),
|
||||
dto.getIsSweetBottom(),
|
||||
dto.getVolume(),
|
||||
new ArrayList<>());
|
||||
waters.put(water.getId(), water);
|
||||
return water;
|
||||
}
|
||||
|
||||
public Water update(UUID id, WaterDto dto) {
|
||||
Water water = waters.get(id);
|
||||
if (dto.getIsSweetBottom() != null)
|
||||
water.setIsSweetBottom(dto.getIsSweetBottom());
|
||||
if (dto.getVolume() != null)
|
||||
water.setVolume(dto.getVolume());
|
||||
return water;
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
if (!waters.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Water not found");
|
||||
}
|
||||
waters.remove(id);
|
||||
}
|
||||
|
||||
public Water addMelon(UUID id, Melon melon) {
|
||||
if (!waters.containsKey(id)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Water not found");
|
||||
}
|
||||
final Water water = waters.get(id);
|
||||
water.getWaterMelons().add(melon);
|
||||
return water;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
spring.application.name=water
|
||||
server.port=8081
|
@ -1,13 +0,0 @@
|
||||
package ru.ulstu.water;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class WaterApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
# Лабораторная работа 5
|
||||
|
||||
## Описание
|
||||
Задание заключается в реализации алгоритмов умножения больших квадратных матриц. Необходимо разработать два алгоритма: последовательный и параллельный. А также провести бенчмарки, а затем описать результаты в отчете.
|
||||
|
||||
**100x100 матрица**:
|
||||
- **4 потока** — наилучший результат.
|
||||
- **10 потоков** — медленнее на почти в половину.
|
||||
- **6 и 8 потоков** — хуже 4 потоков.
|
||||
- **1 и 2 потока** — значительно медленнее.
|
||||
|
||||
**300x300 матрица**:
|
||||
- **4 потока** — лучший результат.
|
||||
- **8 потоков** — чуть хуже.
|
||||
- **10 потоков** — медленнее.
|
||||
- **1 и 2 потока** — значительно медленнее.
|
||||
|
||||
**500x500 матрица**:
|
||||
- **8 потоков** — лучший результат.
|
||||
- **6 и 10 потоков** — немного хуже.
|
||||
- **4 потока** — значительно медленнее.
|
||||
- **1 поток** — самый медленный.
|
||||
|
||||
**Ссылка на демонстрацию работы программы**: https://vk.com/video215756667_456239455?list=ln-z7zFcpvxLexJd3f8ss
|
||||
|
||||
**Вывод**:
|
||||
- Если операция сложнее, рост производительности происходит с увеличением числа потоков.
|
||||
- Слишком много потоков увеличивает накладные расходы (например, 10 потоков). Это может быть связано, например, с:
|
||||
1. **Переключением контекстов**: Когда потоков больше, чем ядер процессора, операционная система часто переключает контексты, что занимает время.
|
||||
2. **Конкуренцией за ресурсы**: Много потоков конкурируют за ограниченные ресурсы, такие как процессорное время и кэш.
|
||||
3. **Управлением потоками**: С увеличением числа потоков растёт нагрузка на систему, связанную с их созданием, управлением и завершением.
|
29
afanasev_dmitry_lab_5/main/.gitignore
vendored
29
afanasev_dmitry_lab_5/main/.gitignore
vendored
@ -1,29 +0,0 @@
|
||||
### IntelliJ IDEA ###
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
@ -1,120 +0,0 @@
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MatrixMultiplier {
|
||||
private final int[][] matrixA;
|
||||
private final int[][] matrixB;
|
||||
private final int[][] result;
|
||||
private final int size;
|
||||
|
||||
public MatrixMultiplier(int size) {
|
||||
this.size = size;
|
||||
this.matrixA = generateMatrix(size);
|
||||
this.matrixB = generateMatrix(size);
|
||||
this.result = new int[size][size];
|
||||
}
|
||||
|
||||
private int[][] generateMatrix(int size) {
|
||||
int[][] matrix = new int[size][size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
for (int j = 0; j < size; j++) {
|
||||
matrix[i][j] = (int) (Math.random() * 10);
|
||||
}
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public void multiplySequential() {
|
||||
for (int i = 0; i < size; i++) {
|
||||
for (int j = 0; j < size; j++) {
|
||||
for (int k = 0; k < size; k++) {
|
||||
result[i][j] += matrixA[i][k] * matrixB[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void multiplyParallel(int numThreads) throws InterruptedException {
|
||||
if (numThreads == 1) {
|
||||
multiplySequential();
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
|
||||
int chunkSize = (int) Math.ceil((double) size / numThreads);
|
||||
|
||||
for (int thread = 0; thread < numThreads; thread++) {
|
||||
final int startRow = thread * chunkSize;
|
||||
final int endRow = Math.min(startRow + chunkSize, size);
|
||||
|
||||
executor.submit(() -> {
|
||||
for (int i = startRow; i < endRow; i++) {
|
||||
for (int j = 0; j < size; j++) {
|
||||
for (int k = 0; k < size; k++) {
|
||||
result[i][j] += matrixA[i][k] * matrixB[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private void resetResult() {
|
||||
for (int i = 0; i < size; i++) {
|
||||
Arrays.fill(result[i], 0);
|
||||
}
|
||||
}
|
||||
|
||||
static class Result {
|
||||
int threads;
|
||||
long time;
|
||||
|
||||
Result(int threads, long time) {
|
||||
this.threads = threads;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
int[] matrixSizes = {100, 300, 500};
|
||||
int[] threadCounts = {1, 2, 4, 6, 8, 10};
|
||||
int runs = 5; // количество прогонов
|
||||
|
||||
for (int size : matrixSizes) {
|
||||
System.out.println("\nРазмер матрицы: " + size + "x" + size);
|
||||
MatrixMultiplier multiplier = new MatrixMultiplier(size);
|
||||
List<Result> results = new ArrayList<>();
|
||||
|
||||
for (int threads : threadCounts) {
|
||||
long totalDuration = 0;
|
||||
|
||||
for (int run = 0; run < runs; run++) {
|
||||
multiplier.resetResult();
|
||||
long startTime = System.nanoTime();
|
||||
multiplier.multiplyParallel(threads);
|
||||
long endTime = System.nanoTime();
|
||||
|
||||
totalDuration += (endTime - startTime);
|
||||
}
|
||||
|
||||
long averageDuration = totalDuration / runs;
|
||||
results.add(new Result(threads, averageDuration));
|
||||
}
|
||||
|
||||
// Сортировка по времени выполнения
|
||||
results.sort(Comparator.comparingLong(r -> r.time));
|
||||
|
||||
System.out.println("Результаты (среднее время за " + runs + " прогонов):");
|
||||
for (Result result : results) {
|
||||
System.out.printf("Потоки: %d, среднее время: %d нс\n", result.threads, result.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class FastDeterminantCalculator {
|
||||
|
||||
public static void main(String[] args) {
|
||||
int[] sizes = {100, 300, 500};
|
||||
int[] threads = {1, 4, 8, 10};
|
||||
|
||||
for (int size : sizes) {
|
||||
BigDecimal[][] matrix = generateMatrix(size);
|
||||
for (int threadCount : threads) {
|
||||
long start = System.currentTimeMillis();
|
||||
BigDecimal determinant = calculateDeterminant(matrix, threadCount);
|
||||
long end = System.currentTimeMillis();
|
||||
System.out.printf("Matrix size: %dx%d, Threads: %d, Time: %d ms\n",
|
||||
size, size, threadCount, (end - start));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static BigDecimal[][] generateMatrix(int size) {
|
||||
BigDecimal[][] matrix = new BigDecimal[size][size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
BigDecimal rowSum = BigDecimal.ZERO;
|
||||
for (int j = 0; j < size; j++) {
|
||||
matrix[i][j] = BigDecimal.valueOf(Math.random() * 10);
|
||||
if (i != j) {
|
||||
rowSum = rowSum.add(matrix[i][j]);
|
||||
}
|
||||
}
|
||||
matrix[i][i] = rowSum.add(BigDecimal.valueOf(Math.random() * 10 + 1));
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public static BigDecimal calculateDeterminant(BigDecimal[][] matrix, int threadCount) {
|
||||
int size = matrix.length;
|
||||
BigDecimal[][] lu = new BigDecimal[size][size];
|
||||
int[] permutations = new int[size];
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
|
||||
if (!luDecomposition(matrix, lu, permutations, executor)) {
|
||||
executor.shutdown();
|
||||
return BigDecimal.ZERO; // Матрица вырожденная
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
|
||||
BigDecimal determinant = BigDecimal.ONE;
|
||||
for (int i = 0; i < size; i++) {
|
||||
determinant = determinant.multiply(lu[i][i]);
|
||||
if (permutations[i] != i) {
|
||||
determinant = determinant.negate(); // Меняем знак при перестановке
|
||||
}
|
||||
}
|
||||
return determinant;
|
||||
}
|
||||
|
||||
public static boolean luDecomposition(BigDecimal[][] matrix, BigDecimal[][] lu, int[] permutations, ExecutorService executor) {
|
||||
int size = matrix.length;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
System.arraycopy(matrix[i], 0, lu[i], 0, size);
|
||||
permutations[i] = i;
|
||||
}
|
||||
|
||||
for (int k = 0; k < size; k++) {
|
||||
int pivot = k;
|
||||
for (int i = k + 1; i < size; i++) {
|
||||
if (lu[i][k].abs().compareTo(lu[pivot][k].abs()) > 0) {
|
||||
pivot = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lu[pivot][k].abs().compareTo(BigDecimal.valueOf(1e-10)) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pivot != k) {
|
||||
BigDecimal[] temp = lu[k];
|
||||
lu[k] = lu[pivot];
|
||||
lu[pivot] = temp;
|
||||
|
||||
int tempPerm = permutations[k];
|
||||
permutations[k] = permutations[pivot];
|
||||
permutations[pivot] = tempPerm;
|
||||
}
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(size - k - 1);
|
||||
for (int i = k + 1; i < size; i++) {
|
||||
int row = i;
|
||||
int finalK = k;
|
||||
executor.submit(() -> {
|
||||
MathContext mc = new MathContext(20, RoundingMode.HALF_UP);
|
||||
lu[row][finalK] = lu[row][finalK].divide(lu[finalK][finalK], mc);
|
||||
for (int j = finalK + 1; j < size; j++) {
|
||||
lu[row][j] = lu[row][j].subtract(lu[row][finalK].multiply(lu[finalK][j], mc));
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
# Лабораторная работа 6
|
||||
|
||||
## Описание
|
||||
Задание заключается в реализации алгоритмов нахождения детерминанта квадратной матрицы. Необходимо разработать два алгоритма: последовательный и параллельный. А также провести бенчмарки, а затем описать результаты в отчете.
|
||||
|
||||
**100x100 матрица**:
|
||||
- **8 потоков** — наилучший результат.
|
||||
- **10 потоков** — результат немного хуже.
|
||||
- **4 потока** — примерно такой же результат как на 10 потоках.
|
||||
- **1 поток** — наихудший результат.
|
||||
|
||||
**300x300 матрица**:
|
||||
- **10 потока** — лучший результат.
|
||||
- **8 потоков** — чуть хуже.
|
||||
- **4 потока** — ещё медленее.
|
||||
- **1 поток** — наихудший результат.
|
||||
|
||||
**500x500 матрица**:
|
||||
- **10 потока** — лучший результат.
|
||||
- **8 потоков** — чуть хуже.
|
||||
- **4 потока** — ещё медленее.
|
||||
- **1 поток** — наихудший результат.
|
||||
|
||||
**Ссылка на демонстрацию работы программы**: https://vkvideo.ru/video215756667_456239456?list=ln-W6TTsYuIRdX8ft7ADr
|
||||
|
||||
**Вывод**:
|
||||
- Если операция сложнее, рост производительности происходит с увеличением числа потоков.
|
||||
- Слишком много потоков увеличивает накладные расходы (замтено только на неочень сложных операциях). Это может быть связано, например, с:
|
||||
1. **Переключением контекстов**: Когда потоков больше, чем ядер процессора, операционная система часто переключает контексты, что занимает время.
|
||||
2. **Конкуренцией за ресурсы**: Много потоков конкурируют за ограниченные ресурсы, такие как процессорное время и кэш.
|
||||
3. **Управлением потоками**: С увеличением числа потоков растёт нагрузка на систему, связанную с их созданием, управлением и завершением.
|
@ -1,13 +0,0 @@
|
||||
|
||||
Балансировка нагрузки — это способ распределения запросов между серверами для предотвращения их перегрузки и обеспечения быстродействия системы.
|
||||
Для этого используются алгоритмы, такие как Round Robin, Least Connections, IP Hash.
|
||||
|
||||
Среди популярных открытых технологий — Nginx, HAProxy и Traefik. Nginx часто работает как реверс-прокси,
|
||||
распределяя запросы и обрабатывая SSL. HAProxy подходит для высоконагруженных систем, а Traefik автоматически
|
||||
настраивает маршрутизацию в облачных кластерах.
|
||||
|
||||
В базах данных балансировка нагрузки позволяет направлять запросы на чтение к репликам, а на запись — к основному узлу.
|
||||
Инструменты, такие как ProxySQL, помогают автоматизировать этот процесс.
|
||||
|
||||
Реверс-прокси не только распределяет нагрузку, но и повышает безопасность системы, скрывая её внутреннюю архитектуру.
|
||||
Таким образом, открытые технологии играют ключевую роль в создании масштабируемых и надёжных систем.
|
@ -1,15 +0,0 @@
|
||||
Распределённые системы являются основой современных сервисов, включая социальные сети. Их устройство предполагает разделение задач на микросервисы,
|
||||
где каждый компонент выполняет узкоспециализированную функцию. Это упрощает разработку, позволяет масштабировать только необходимые части системы и
|
||||
делает её более устойчивой к сбоям.
|
||||
|
||||
Для управления такими системами используются инструменты оркестрации, например, Kubernetes и Docker Swarm. Они автоматизируют развёртывание,
|
||||
масштабирование и обновление сервисов, упрощая сопровождение. Однако их использование требует опыта и может осложнить отладку.
|
||||
|
||||
Очереди сообщений, такие как RabbitMQ или Kafka, помогают асинхронно передавать данные между сервисами. Это снижает нагрузку и обеспечивает надёжное взаимодействие,
|
||||
передавая запросы, уведомления или данные для обработки.
|
||||
|
||||
Распределённые системы обладают преимуществами в виде масштабируемости, устойчивости и гибкости разработки.
|
||||
Однако их сложность может стать серьёзным вызовом при проектировании и сопровождении.
|
||||
|
||||
Параллельные вычисления полезны, например, для обработки больших объёмов данных или машинного обучения,
|
||||
но в некоторых случаях последовательная обработка более предпочтительна. Такой подход требует анализа задач, чтобы избежать излишней сложности.
|
@ -1,69 +0,0 @@
|
||||
services:
|
||||
wp_db:
|
||||
|
||||
image: mariadb:10.6.4-focal
|
||||
command: '--default-authentication-plugin=mysql_native_password'
|
||||
volumes:
|
||||
- wp_db_data:/var/lib/mysql
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=somewordpress
|
||||
- MYSQL_DATABASE=wordpress
|
||||
- MYSQL_USER=wordpress
|
||||
- MYSQL_PASSWORD=wordpress
|
||||
expose:
|
||||
- 3306
|
||||
- 33060
|
||||
wordpress:
|
||||
image: wordpress:latest
|
||||
volumes:
|
||||
- wp_data:/var/www/html
|
||||
ports:
|
||||
- 52384:80
|
||||
restart: always
|
||||
environment:
|
||||
- WORDPRESS_DB_HOST=wp_db
|
||||
- WORDPRESS_DB_USER=wordpress
|
||||
- WORDPRESS_DB_PASSWORD=wordpress
|
||||
- WORDPRESS_DB_NAME=wordpress
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: db
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: 1234
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql
|
||||
|
||||
redmine:
|
||||
image: redmine:latest
|
||||
container_name: redmine
|
||||
ports:
|
||||
- "11001:3000"
|
||||
environment:
|
||||
- REDMINE_DB_POSTGRESQL=db
|
||||
- REDMINE_DB_DATABASE=redmine
|
||||
- REDMINE_DB_USERNAME=posgres
|
||||
- REDMINE_DB_PASSWORD=1234
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
ports:
|
||||
- "11002:3000"
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
volumes:
|
||||
- gitea_data:/data
|
||||
|
||||
volumes:
|
||||
wp_db_data:
|
||||
wp_data:
|
||||
db_data:
|
||||
redmine_db_data:
|
||||
gitea_data:
|
@ -1,126 +0,0 @@
|
||||
▎Лабораторная работа №1 - Знакомство с Docker и Docker Compose
|
||||
|
||||
Цель: Изучение современных технологий контейнеризации.
|
||||
|
||||
▎Задачи:
|
||||
|
||||
1. Установить Docker.
|
||||
|
||||
2. Изучить применение и принципы работы Docker.
|
||||
|
||||
3. Изучить утилиту Docker Compose и структуру файла docker-compose.yml.
|
||||
|
||||
4. Развернуть не менее трех различных сервисов с помощью Docker Compose.
|
||||
|
||||
▎Разворачивание сервисов
|
||||
|
||||
Необходимо развернуть минимум три сервиса из предложенного списка:
|
||||
|
||||
|
||||
1. Redmine - система учета багов (баг-трекер).
|
||||
|
||||
2. WordPress - популярная система управления контентом.
|
||||
|
||||
3. Gitea - сервис для хранения git-репозиториев.
|
||||
|
||||
▎Требования к Docker Compose:
|
||||
|
||||
• Несколько контейнеров.
|
||||
|
||||
• Хотя бы один volume.
|
||||
|
||||
• Хотя бы один проброшенный порт на хост.
|
||||
|
||||
Система должна быть развернута полностью, включая создание администратора и корректное функционирование, что будет подтверждено скриншотами.
|
||||
|
||||
▎Ход работы
|
||||
|
||||
1. Установил Docker Desktop на Windows.
|
||||
|
||||
2. Проверил установку с помощью команды docker --version.
|
||||
|
||||
3. Развернул сервисы (описаны ниже).
|
||||
|
||||
▎Объяснение работы кода:
|
||||
|
||||
▎WordPress:
|
||||
|
||||
1. Образ сборки:
|
||||
image: wordpress:latest - используется последний официальный образ WordPress.
|
||||
|
||||
|
||||
2. Имя контейнера:
|
||||
container_name: wordpress - имя контейнера устанавливается как wordpress.
|
||||
|
||||
|
||||
3. Проброс портов:
|
||||
Порт 80 контейнера пробрасывается на порт 8080 хоста.
|
||||
|
||||
|
||||
4. Тома для хранения данных:
|
||||
|
||||
volumes:
|
||||
- wordpress_data:/var/www/html
|
||||
|
||||
Том wordpress_data монтируется в директорию /var/www/html контейнера для хранения данных WordPress.
|
||||
|
||||
|
||||
5. Переменные окружения для WordPress:
|
||||
|
||||
environment:
|
||||
WORDPRESS_DB_HOST: db
|
||||
WORDPRESS_DB_USER: example_user
|
||||
WORDPRESS_DB_PASSWORD: example_password
|
||||
WORDPRESS_DB_NAME: example_db
|
||||
|
||||
Параметры для подключения к базе данных.
|
||||
|
||||
6. Зависимость от базы данных:
|
||||
depends_on: db - указывает, что WordPress зависит от контейнера базы данных (db).
|
||||
|
||||
▎Redmine:
|
||||
|
||||
1. Образ сборки:
|
||||
image: redmine:latest - используется последний официальный образ Redmine.
|
||||
|
||||
|
||||
2. Имя контейнера:
|
||||
container_name: redmine - имя контейнера устанавливается как redmine.
|
||||
|
||||
|
||||
3. Проброс портов:
|
||||
ports:"8081:3000" - порт 3000 контейнера пробрасывается на порт 8081 хоста.
|
||||
|
||||
|
||||
4. Переменные окружения для Redmine:
|
||||
|
||||
environment:
|
||||
REDMINE_DB_MYSQL: redmine_db
|
||||
REDMINE_DB_USERNAME: redmine_user
|
||||
REDMINE_DB_PASSWORD: redmine_password
|
||||
|
||||
Параметры для подключения к базе данных.
|
||||
|
||||
5. Зависимость от базы данных:
|
||||
depends_on: - redmine_db - Redmine зависит от контейнера с базой данных.
|
||||
|
||||
▎Gitea:
|
||||
|
||||
1. Образ сборки:
|
||||
image: gitea/gitea:latest - используется последний официальный образ Gitea.
|
||||
|
||||
|
||||
2. Имя контейнера:
|
||||
container_name: gitea - имя контейнера устанавливается как gitea.
|
||||
|
||||
|
||||
3. Проброс портов:
|
||||
ports:"8082:3000" - порт 3000 контейнера пробрасывается на порт 8082 хоста.
|
||||
|
||||
|
||||
4. Тома для хранения данных:
|
||||
|
||||
volumes:
|
||||
- gitea_data:/data
|
||||
|
||||
[Видео](https://disk.yandex.ru/d/JFWkukJKwbfhIw)
|
@ -1,18 +0,0 @@
|
||||
# docker-compose.yml
|
||||
|
||||
services:
|
||||
service-1:
|
||||
build:
|
||||
context: ./service_1
|
||||
volumes:
|
||||
- ./data:/var/data
|
||||
- ./result:/var/result
|
||||
|
||||
service-2:
|
||||
build:
|
||||
context: ./service_2
|
||||
volumes:
|
||||
- ./data:/var/data
|
||||
- ./result:/var/result
|
||||
depends_on:
|
||||
- service-1
|
@ -1,35 +0,0 @@
|
||||
## Вариант 1 сервиса
|
||||
0. Ищет в каталоге /var/data самый большой по объёму файл и перекладывает его в /var/result/data.txt.
|
||||
|
||||
## Вариант 2 сервиса
|
||||
0. Сохраняет произведение первого и последнего числа из файла /var/data/data.txt в /var/result/result.txt.
|
||||
|
||||
Для обоих приложений создадим Dockerfile. Вот пример для **service-1** файл для **service-2** будет идентичен, из-за одной версии питона и одного набора библеотек
|
||||
(используются только стандартная библиотека):
|
||||
|
||||
|
||||
|
||||
Пояснение:
|
||||
- **Stage 1**: Мы используем `python:3.10-slim` как образ для сборки, где копируем файл `main.py` и устанавливаем зависимости, если это необходимо.
|
||||
- **Stage 2**: В этом слое мы копируем скомпилированные файлы из предыдущего этапа и определяем команду для запуска приложения.
|
||||
|
||||
Аналогичный Dockerfile будет для **service_2**.
|
||||
|
||||
### Docker Compose файл
|
||||
|
||||
Теперь нужно настроить файл `docker-compose.yml`, который позволит запустить оба приложения:
|
||||
|
||||
|
||||
|
||||
Пояснение:
|
||||
- **services**: Мы объявляем два сервиса — `service_1` и `service_2`.
|
||||
- **build**: Указываем контекст сборки для каждого сервиса (директории, где находятся Dockerfile и код).
|
||||
- **volumes**: Монтируем локальные директории `./data` и `./result` в контейнеры, чтобы обмениваться файлами между сервисами.
|
||||
- **depends_on**: Задаем зависимость `service_2` от `service_1`, чтобы второй сервис запускался только после первого.
|
||||
|
||||
### Заключение
|
||||
|
||||
Это пример, как можно реализовать простейшее распределённое приложение с использованием Docker. Первое приложение генерирует данные для второго, который обрабатывает их и записывает результат в файл. Docker и Docker Compose позволяют легко управлять и изолировать каждое приложение.ker Compose для запуска двух программ, обрабатывающих данные в контейнерах.
|
||||
|
||||
|
||||
[Видео](https://disk.yandex.ru/d/FFqx6_tdtX8s-g)
|
@ -1,11 +0,0 @@
|
||||
FROM python:3.10-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./main.py .
|
||||
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/main.py .
|
||||
|
||||
CMD ["python", "main.py"]
|
@ -1,39 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def find_largest_file(directory):
|
||||
largest_file = None
|
||||
largest_size = 0
|
||||
|
||||
# Проходим по всем файлам и подкаталогам в указанном каталоге
|
||||
for dirpath, dirnames, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
try:
|
||||
# Получаем размер файла
|
||||
file_size = os.path.getsize(filepath)
|
||||
# Проверяем, является ли этот файл самым большим
|
||||
if file_size > largest_size:
|
||||
largest_size = file_size
|
||||
largest_file = filepath
|
||||
except OSError as e:
|
||||
print(f"Ошибка при доступе к файлу {filepath}: {e}")
|
||||
|
||||
return largest_file
|
||||
|
||||
def main():
|
||||
source_directory = '/var/data'
|
||||
destination_file = '/var/result/data.txt'
|
||||
|
||||
largest_file = find_largest_file(source_directory)
|
||||
|
||||
if largest_file:
|
||||
print(f"Самый большой файл: {largest_file} ({os.path.getsize(largest_file)} байт)")
|
||||
# Копируем самый большой файл в указанное место
|
||||
shutil.copy(largest_file, destination_file)
|
||||
print(f"Файл скопирован в: {destination_file}")
|
||||
else:
|
||||
print("Не найдено ни одного файла.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,11 +0,0 @@
|
||||
FROM python:3.10-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY ./main.py .
|
||||
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/main.py .
|
||||
|
||||
CMD ["python", "main.py"]
|
@ -1,42 +0,0 @@
|
||||
def read_numbers_from_file(file_path):
|
||||
try:
|
||||
with open(file_path, 'r') as file:
|
||||
# Читаем все строки и преобразуем их в числа
|
||||
numbers = [float(line.strip()) for line in file.read().split() if
|
||||
line.strip().isdigit() or (
|
||||
line.strip().replace('.', '', 1).isdigit() and line.strip().count('.') < 2)]
|
||||
return numbers
|
||||
except FileNotFoundError:
|
||||
print(f"Файл {file_path} не найден.")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Произошла ошибка при чтении файла: {e}")
|
||||
return []
|
||||
|
||||
def save_result_to_file(file_path, result):
|
||||
try:
|
||||
with open(file_path, 'w') as file:
|
||||
file.write(str(result))
|
||||
except Exception as e:
|
||||
print(f"Произошла ошибка при записи в файл: {e}")
|
||||
|
||||
def main():
|
||||
input_file = '/var/result/data.txt'
|
||||
output_file = '/var/result/result.txt'
|
||||
|
||||
numbers = read_numbers_from_file(input_file)
|
||||
|
||||
if numbers:
|
||||
first_number = numbers[0]
|
||||
last_number = numbers[-1]
|
||||
product = first_number * last_number
|
||||
|
||||
print(f"Первое число: {first_number}, Последнее число: {last_number}, Произведение: {product}")
|
||||
|
||||
save_result_to_file(output_file, product)
|
||||
print(f"Результат сохранён в {output_file}")
|
||||
else:
|
||||
print("Не удалось получить числа из файла.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,10 +0,0 @@
|
||||
FROM python:3.11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "--host", "0.0.0.0", "main:app", "--port", "8001"]
|
@ -1,21 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DB_HOST = os.environ.get("DB_HOST")
|
||||
DB_PORT = os.environ.get("DB_PORT")
|
||||
DB_NAME = os.environ.get("DB_NAME")
|
||||
DB_USER = os.environ.get("DB_USER")
|
||||
DB_PASS = os.environ.get("DB_PASS")
|
||||
|
||||
|
||||
SECRET_AUTH_TOKEN = os.environ.get("SECRET_AUTH_TOKEN")
|
||||
SECRET_VERIFICATE_TOKEN = os.environ.get("SECRET_VERIFICATE_TOKEN")
|
||||
|
||||
DEVICE_START_COUNTER = os.environ.get("DEVICE_START_COUNTER")
|
||||
|
||||
logging.getLogger('passlib').setLevel(logging.ERROR)
|
||||
|
@ -1,49 +0,0 @@
|
||||
import time
|
||||
from sys import gettrace
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config import DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Table, Column, Integer, String, TIMESTAMP, ForeignKey, Boolean, MetaData, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
def fill_from_model(self, model: BaseModel | dict):
|
||||
"""Заполняет все поля из словаря или модели, помечая объект 'грязным' для корректного обновления"""
|
||||
if isinstance(model, BaseModel):
|
||||
items = model.model_dump().items()
|
||||
else:
|
||||
items = model.items()
|
||||
|
||||
for key, value in items:
|
||||
setattr(self, key, value)
|
||||
|
||||
DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=gettrace() is not None)
|
||||
async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def begin_transactions() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker().begin() as transaction:
|
||||
try:
|
||||
yield transaction.session
|
||||
except Exception as e:
|
||||
await transaction.rollback()
|
||||
raise e
|
||||
await transaction.commit()
|
@ -1,5 +0,0 @@
|
||||
from .models import *
|
||||
from .router import *
|
||||
from .schemas import *
|
||||
from .dependencies import *
|
||||
from .websocket_auth import *
|
@ -1,31 +0,0 @@
|
||||
"""Модуль содержащий общие схемы для хаба и устройства во избежание цикличного импорта"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
__all__ = ["RoleCreate", "RoleRead", "ActiveDeviceRead", "ActiveDeviceCreate", "ModelRead"]
|
||||
|
||||
|
||||
class ModelRead(BaseModel, from_attributes=True, frozen=True):
|
||||
name: str
|
||||
version: str
|
||||
|
||||
|
||||
class RoleCreate(BaseModel):
|
||||
name: str
|
||||
permissions: Optional[dict]
|
||||
|
||||
|
||||
class RoleRead(RoleCreate, from_attributes=True):
|
||||
id: int
|
||||
|
||||
|
||||
class ActiveDeviceCreate(BaseModel, from_attributes=True):
|
||||
last_connection: Optional[datetime] = None
|
||||
role: RoleCreate = Field(default_factory=lambda: RoleCreate(name="admin", permissions={"include_all": True}))
|
||||
|
||||
|
||||
class ActiveDeviceRead(ActiveDeviceCreate, from_attributes=True):
|
||||
# id: uuid.UUID todo насколько необходимо
|
||||
role: Optional[RoleRead] = None
|
@ -1,207 +0,0 @@
|
||||
import hashlib
|
||||
import uuid
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from fastapi import Depends, Path, HTTPException
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import Table, Column, Integer, String, TIMESTAMP, ForeignKey, JSON, Boolean, MetaData, DateTime, UUID, \
|
||||
ARRAY, DECIMAL, DOUBLE_PRECISION
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload, relationship
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
current_user = lambda: None
|
||||
|
||||
|
||||
from database import get_async_session, Base
|
||||
from jwt_config import tokens_data, denylist
|
||||
from .schemas import DeviceRead, DeviceWebsocketAuth, JwtAccessClaimsForDeviceRequests, \
|
||||
JwtRefreshClaimsForDeviceRequests, TokensRead, TokenDataServer
|
||||
|
||||
from device.models import Device, ActiveDevice, DeviceForInherit, Hub
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
|
||||
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import get_async_session
|
||||
|
||||
class User(SQLAlchemyBaseUserTable[uuid.UUID], Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
surname = Column(String, nullable=False)
|
||||
patronymic = Column(String, nullable=True)
|
||||
creation_on = Column(TIMESTAMP, default=datetime.utcnow)
|
||||
last_entry = Column(TIMESTAMP, nullable=True)
|
||||
hashed_password = Column(String(length=1024), nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_superuser = Column(Boolean, default=False, nullable=False)
|
||||
is_verified = Column(Boolean, default=False, nullable=False)
|
||||
token = Column(String, nullable=True)
|
||||
fcm_token = Column(String, nullable=True) # Идентификатор устройства для уведомлений
|
||||
hashes_rfids = Column(ARRAY(String), default=[])
|
||||
face_embedding = Column(ARRAY(DOUBLE_PRECISION), nullable=True)
|
||||
|
||||
devices = relationship("Device", secondary="active_devices", back_populates="users", viewonly=True)
|
||||
active_devices = relationship("ActiveDevice")
|
||||
group = relationship("Group", uselist=False, back_populates="users")
|
||||
notifications = relationship("Notification", back_populates="user")
|
||||
|
||||
async def include_relationship_for_read(self, session: AsyncSession):
|
||||
await session.refresh(self, ["group"])
|
||||
return self
|
||||
|
||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||
yield SQLAlchemyUserDatabase(session, User)
|
||||
|
||||
|
||||
|
||||
|
||||
__all__ = ["get_active_devices", "get_active_devices_in_schema", "jwt_websocket_create_access_token", "get_tokens",
|
||||
"get_tokens_from_refresh",
|
||||
"optional_verifyed_access_token", "verifyed_access_token"]
|
||||
|
||||
|
||||
async def get_active_devices(user: User = Depends(current_user),
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
cast_to_schema: bool = False) -> List[Device | DeviceRead]:
|
||||
tables = [table for table in DeviceForInherit.__subclasses__() if table is not Device and table is not Hub]
|
||||
stmt = (
|
||||
select(Device, ActiveDevice, *tables) # только one-to-one relathionship, иначе будет дублирование строк
|
||||
.join(Device.active_devices)
|
||||
.options(joinedload(ActiveDevice.role, innerjoin=True),
|
||||
joinedload(Device.model))
|
||||
.where(ActiveDevice.user_id == user.id)
|
||||
.where(Device.activated == True)
|
||||
.join(Device.hub, isouter=True)
|
||||
)
|
||||
for table in tables:
|
||||
stmt = stmt.join(table, table.device_id == Device.id)
|
||||
print("count join:", str(stmt).lower().count("join"))
|
||||
devices = []
|
||||
async for device, active_device, *inherit_devices in await session.stream(stmt):
|
||||
inherit_device = next((i for i in inherit_devices if i is not None), None)
|
||||
device.__dict__["active_device"] = active_device
|
||||
if inherit_device is None:
|
||||
devices.append(device.get_schema() if cast_to_schema else inherit_device)
|
||||
else:
|
||||
inherit_device.device = device
|
||||
devices.append(inherit_device.get_schema() if cast_to_schema else inherit_device)
|
||||
return devices
|
||||
|
||||
|
||||
async def get_active_devices_in_schema(user: User = Depends(current_user),
|
||||
session: AsyncSession = Depends(get_async_session)) -> list[DeviceRead]:
|
||||
return await get_active_devices(user, session, True)
|
||||
|
||||
|
||||
def jwt_websocket_create_access_token(device: DeviceWebsocketAuth,
|
||||
_=Depends(current_user),
|
||||
authorize = None) -> str:
|
||||
# Получение токена по пользователю для подключения устройства по вебсокетам
|
||||
# При необходимости можно добавить ограничение по количествку токенов на пользователя или на устройства
|
||||
return authorize.create_access_token(device.macaddress, user_claims=device.model_dump(), expires_time=False)
|
||||
|
||||
|
||||
def jwt_request_create_access_token(claims: JwtAccessClaimsForDeviceRequests, authorize: None = Depends()) -> str:
|
||||
return authorize.create_access_token(str(claims.device_id), user_claims=claims.model_dump(exclude="device_id"))
|
||||
|
||||
|
||||
def jwt_request_create_refresh_token(claims: JwtRefreshClaimsForDeviceRequests, authorize: None = Depends()) -> str:
|
||||
return authorize.create_refresh_token(claims.part_access, user_claims=claims.model_dump(mode='json'))
|
||||
|
||||
|
||||
def get_tokens(device_id: uuid.UUID, authorize: None) -> TokensRead:
|
||||
access_token = jwt_request_create_access_token(JwtAccessClaimsForDeviceRequests(device_id=device_id), authorize)
|
||||
refresh_token = jwt_request_create_refresh_token(
|
||||
JwtRefreshClaimsForDeviceRequests(device_id=device_id, part_access=access_token[:30]),
|
||||
authorize
|
||||
)
|
||||
tokens_data[authorize.get_raw_jwt(refresh_token)['jti']] = TokenDataServer.default()
|
||||
return TokensRead(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token
|
||||
)
|
||||
|
||||
|
||||
def verifyed_refresh_token(access_token: str, refresh_token, counter_hash: str,
|
||||
authorize = None) -> Tuple[JwtRefreshClaimsForDeviceRequests, TokenDataServer]:
|
||||
authorize.jwt_refresh_token_required("websocket", token=refresh_token)
|
||||
jwt_raw_refresh_token = authorize.get_raw_jwt(refresh_token)
|
||||
jwt_raw_access_token = authorize.get_raw_jwt(access_token)
|
||||
model = JwtRefreshClaimsForDeviceRequests.model_validate(jwt_raw_refresh_token)
|
||||
|
||||
if model.part_access != access_token[:30]:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="part access token does not match input access token"
|
||||
)
|
||||
token_data = tokens_data.get(jwt_raw_refresh_token['jti'])
|
||||
# todo Если токены не сохраняться при падении сервера, тогда валидации не должно быть,
|
||||
# если сохраняются всегда должны находиться данные по токену
|
||||
if token_data is None:
|
||||
token_data = TokenDataServer.default()
|
||||
# raise HTTPException(
|
||||
# status_code=403,
|
||||
# detail="refresh token no save in server. Get refresh_token again in /get_tokens_from_user"
|
||||
# )
|
||||
if token_data.hash_counter != counter_hash:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="invalid counter hash"
|
||||
)
|
||||
# Проверка прошла успешно
|
||||
data = TokenDataServer(
|
||||
counter=token_data.counter + 1,
|
||||
hash_counter=hashlib.md5(str(token_data.counter + 1).encode()).hexdigest()
|
||||
)
|
||||
# Токены больше не актуальны, добавляем в чс
|
||||
tokens_data.pop(jwt_raw_refresh_token['jti'], None)
|
||||
tokens_data.pop(jwt_raw_access_token['jti'], None)
|
||||
denylist.update({
|
||||
jwt_raw_refresh_token['jti'],
|
||||
jwt_raw_access_token['jti']
|
||||
})
|
||||
return model, data
|
||||
|
||||
|
||||
def verifyed_access_token(token: str | None, authorize = None
|
||||
) -> JwtAccessClaimsForDeviceRequests:
|
||||
# Добавить доп валидацию при необходимости
|
||||
authorize.jwt_required("websocket", token)
|
||||
raw = authorize.get_raw_jwt(token)
|
||||
return JwtAccessClaimsForDeviceRequests.model_validate(raw | {"device_id": raw["sub"]})
|
||||
|
||||
|
||||
def optional_verifyed_access_token(token: str | None = None, authorize = None
|
||||
) -> JwtAccessClaimsForDeviceRequests | None:
|
||||
try:
|
||||
return verifyed_access_token(token, authorize)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_tokens_from_refresh(model_data: JwtRefreshClaimsForDeviceRequests = Depends(verifyed_refresh_token),
|
||||
authorize = None) -> TokensRead:
|
||||
model, data = model_data
|
||||
result = get_tokens(model.device_id, authorize)
|
||||
tokens_data[authorize.get_raw_jwt(result.refresh_token)['jti']] = data
|
||||
return result
|
||||
|
||||
|
||||
async def device_activate(token: Optional[JwtAccessClaimsForDeviceRequests] = Depends(verifyed_access_token),
|
||||
session: AsyncSession = Depends(get_async_session)) -> Device:
|
||||
|
||||
device = await session.get(Device, ident=token.device_id)
|
||||
if device is None:
|
||||
raise EntityNotFound(Device, device_id=token.device_id, **token.model_dump()).get()
|
||||
if device.activated:
|
||||
raise HTTPException(status_code=400, detail="Устройство уже активировано")
|
||||
device.activated = True
|
||||
await session.commit()
|
||||
return device
|
@ -1,158 +0,0 @@
|
||||
import datetime
|
||||
import abc
|
||||
import uuid
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import (Table, Column, Integer, String, TIMESTAMP, ForeignKey, Boolean, MetaData,
|
||||
UUID, DateTime, Index, JSON, func)
|
||||
from sqlalchemy.orm import relationship, class_mapper
|
||||
from sqlalchemy.dialects.postgresql import MACADDR8, CIDR
|
||||
|
||||
from database import Base
|
||||
|
||||
__all__ = ["Device", "Model", "ActiveDevice", "Role", "DeviceForInherit", "Hub"]
|
||||
|
||||
|
||||
from .schemas import DeviceCreate, ActiveDeviceCreate, DeviceRead
|
||||
|
||||
current_user = lambda: None
|
||||
|
||||
class DeviceForInherit(Base):
|
||||
__abstract__ = True
|
||||
|
||||
@classmethod
|
||||
def create_from(cls, schema: DeviceCreate,
|
||||
active_devices: list[ActiveDeviceCreate] | ActiveDeviceCreate | None = None,
|
||||
user_id_in_active_devices: uuid.UUID | None = None) -> 'DeviceForInherit':
|
||||
# overrides in inherits models
|
||||
if not isinstance(schema, (DeviceCreate, "HubCreate")):
|
||||
raise TypeError(f"schema must be a DeviceCreate or inherit from him, not {type(schema)}")
|
||||
if cls == Device:
|
||||
return cls.create_device(schema, active_devices, user_id_in_active_devices)
|
||||
result = cls()
|
||||
result.__dict__.update(schema.model_dump())
|
||||
result.device = cls.create_device(schema, active_devices, user_id_in_active_devices)
|
||||
return result
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_schema(self) -> DeviceRead: # overrides
|
||||
pass
|
||||
|
||||
def get_schema_from_type(self, T: Type[DeviceRead]) -> DeviceRead:
|
||||
if 'device' not in self.__dict__:
|
||||
raise ValueError(f'Property device must be loaded {self.__dict__}')
|
||||
if 'hub' not in self.device.__dict__ and 'hub_id' in self.device.__dict__:
|
||||
self.device.__dict__['hub'] = self.device.hub_id
|
||||
elif isinstance(self.device.hub, Hub):
|
||||
self.device.__dict__["hub"] = self.device.hub.__dict__ | self.device.hub.device.__dict__
|
||||
return T.model_validate(self.__dict__ | self.device.__dict__ | {"type_name": type(self).__name__})
|
||||
|
||||
@staticmethod
|
||||
def create_device(schema: DeviceCreate,
|
||||
active_devices: list[ActiveDeviceCreate] | ActiveDeviceCreate | None = None,
|
||||
user_id_in_active_devices: uuid.UUID | None = None) -> 'Device':
|
||||
schema_model = schema.model_dump(exclude={'hub', "model"})
|
||||
device = Device(
|
||||
**{key: value for key, value in schema_model.items() if key in class_mapper(Device).attrs.keys()},
|
||||
)
|
||||
if hasattr(schema, 'hub') and isinstance(schema.hub, uuid.UUID):
|
||||
device.hub_id = schema.hub
|
||||
elif hasattr(schema, 'hub') and schema.hub is not None:
|
||||
device.hub = Hub.create_from(schema.hub, active_devices, user_id_in_active_devices)
|
||||
if isinstance(active_devices, list):
|
||||
device.active_devices = [ActiveDevice.create_from(i, user_id_in_active_devices) for i in active_devices]
|
||||
elif isinstance(active_devices, ActiveDeviceCreate):
|
||||
device.active_devices = [ActiveDevice.create_from(active_devices, user_id_in_active_devices)]
|
||||
if schema.model is not None:
|
||||
device.model = Model(**schema.model.model_dump())
|
||||
return device
|
||||
|
||||
|
||||
class Device(DeviceForInherit, Base):
|
||||
__tablename__ = 'devices'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
model_id = Column(ForeignKey("models.id"), nullable=True)
|
||||
|
||||
# use_alter - для разрешения цикличных зависимостей nullable - True, чтобы у хаба не было хаба
|
||||
# (ограничение для остальных устройств будет на уровне схем)
|
||||
# name - имя ограничения для субд и нормальной работы миграций и автотестов
|
||||
hub_id = Column(ForeignKey("hubs.device_id", use_alter=True, name="fk_device_hub_id"), nullable=True)
|
||||
|
||||
first_connection_date = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
last_connection_date = Column(DateTime, server_default=func.now(), onupdate=func.current_timestamp())
|
||||
macaddress = Column(MACADDR8, nullable=False)
|
||||
serial_number = Column(Integer, nullable=False)
|
||||
configuration = Column(JSON, nullable=False, default={})
|
||||
is_active = Column(Boolean, nullable=False)
|
||||
activated = Column(Boolean, nullable=False, default=False, index=True) # Устройство подключилось к серверу
|
||||
|
||||
hub = relationship("Hub", back_populates="connected_devices", uselist=False, foreign_keys=[hub_id])
|
||||
model = relationship('Model', uselist=False, cascade="all,delete",)
|
||||
active_devices = relationship("ActiveDevice", cascade="all,delete", back_populates="device")
|
||||
users = relationship('User', secondary='active_devices', back_populates="devices", viewonly=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('unique_phys_device', "serial_number", "macaddress", unique=True),
|
||||
)
|
||||
|
||||
def get_schema(self) -> DeviceRead:
|
||||
return self.get_schema_from_type(DeviceRead)
|
||||
|
||||
|
||||
class Hub(DeviceForInherit):
|
||||
__tablename__ = 'hubs'
|
||||
|
||||
device_id = Column(ForeignKey('devices.id'), primary_key=True)
|
||||
ip_address = Column(CIDR, nullable=False)
|
||||
port = Column(Integer, nullable=False)
|
||||
|
||||
device = relationship("Device", uselist=False, foreign_keys=[device_id])
|
||||
connected_devices = relationship("Device", foreign_keys=[Device.hub_id], back_populates="hub")
|
||||
|
||||
def get_schema(self) -> "HubRead":
|
||||
return self.get_schema_from_type("HubRead")
|
||||
|
||||
|
||||
class Model(Base):
|
||||
__tablename__ = 'models'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
version = Column(String, nullable=False)
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
permissions = Column(JSON, nullable=False) # todo продумать структуру
|
||||
|
||||
active_device = relationship("ActiveDevice", uselist=False, back_populates="role")
|
||||
|
||||
|
||||
class ActiveDevice(Base):
|
||||
__tablename__ = 'active_devices'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(ForeignKey('users.id'))
|
||||
device_id = Column(ForeignKey('devices.id'))
|
||||
role_id = Column(ForeignKey('roles.id'), nullable=False)
|
||||
last_connection = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
|
||||
device = relationship(Device, uselist=False, back_populates="active_devices")
|
||||
role = relationship(Role, uselist=False, back_populates="active_device")
|
||||
user = relationship("User", uselist=False, back_populates="active_devices")
|
||||
|
||||
__table_args__ = (
|
||||
Index('fk_index', "user_id", "device_id", unique=True),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_from(schema: ActiveDeviceCreate, user_id: uuid.UUID | None = None) -> "ActiveDevice":
|
||||
md = schema.model_dump(exclude={'role'})
|
||||
if user_id is not None:
|
||||
md['user_id'] = user_id
|
||||
return ActiveDevice(role=Role(**schema.role.model_dump()), **md)
|
@ -1,154 +0,0 @@
|
||||
from typing import List, Any, Optional
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from jwt_config import denylist, jwt_websocket_required_auth
|
||||
from . import Device
|
||||
from .dependencies import get_active_devices_in_schema, get_tokens, get_tokens_from_refresh, device_activate
|
||||
from .schemas import DeviceRead, DeviceWebsocketAuth, TokensRead
|
||||
|
||||
|
||||
current_user = lambda: None
|
||||
|
||||
|
||||
__all__ = ["router", "manager", "denylist", "websocket_router"]
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/device",
|
||||
tags=["Device"]
|
||||
)
|
||||
websocket_router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/active_devices")
|
||||
async def get_all_active_devices_for_user(active_devices=Depends(get_active_devices_in_schema)
|
||||
) -> List[DeviceRead | None]:
|
||||
"""Получает все доступные **активированные** устройства всех видов, связанные с текущем пользователем.
|
||||
Внутри сущностей храниться **active_device**, в котором обозначена взаимодействие устройства с конкретным пользователем"""
|
||||
return active_devices
|
||||
|
||||
|
||||
@router.get("/token_from_user")
|
||||
async def get_tokens_for_device_from_user(
|
||||
tokens: TokensRead = Depends(get_tokens),
|
||||
_=Depends(current_user)) -> TokensRead:
|
||||
"""Генерирует токены по переданому **device_id**, валидация devie_id не происходит,
|
||||
просто токен с невалидным id нельзя будет нигде применить"""
|
||||
return tokens
|
||||
|
||||
|
||||
@router.patch("/activate", responses={400: {"description": "Устройство уже активировано"}})
|
||||
async def activate_added_device(device: Device = Depends(device_activate)) -> DeviceRead:
|
||||
return device.__dict__
|
||||
|
||||
|
||||
@router.get("/refresh_token", responses={
|
||||
401: {"description": "Некорректно переданные токены"},
|
||||
403: {"description": "Некорректно переданные токены"}
|
||||
})
|
||||
async def refresh_tokens(
|
||||
tokens: TokensRead = Depends(get_tokens_from_refresh)) -> TokensRead:
|
||||
return tokens
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: dict[DeviceWebsocketAuth, WebSocket] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket,
|
||||
token: str,
|
||||
authorize = None
|
||||
) -> Optional[DeviceWebsocketAuth]:
|
||||
|
||||
decoded_token, device = jwt_websocket_required_auth(token, authorize)
|
||||
|
||||
await websocket.accept()
|
||||
old_websocket = self.active_connections.get(device) # теоретически на одно устройство одно вебсокет подключение
|
||||
if old_websocket is not None:
|
||||
await old_websocket.close()
|
||||
self.active_connections[device] = websocket
|
||||
denylist.add(decoded_token['jti'])
|
||||
return device
|
||||
|
||||
def disconnect(self, device: DeviceWebsocketAuth):
|
||||
self.active_connections.pop(device, None)
|
||||
|
||||
async def send_message_to_device(self, device: DeviceWebsocketAuth, message: str | Any) -> WebSocket:
|
||||
device_auth = DeviceWebsocketAuth.model_validate(device, from_attributes=True)
|
||||
websocket = self.active_connections.get(device_auth)
|
||||
if websocket is None:
|
||||
raise HTTPException(status_code=404, detail={
|
||||
"msg": "Устройство, статус которого пытаются изменить, не подключен к серверу",
|
||||
"device": jsonable_encoder(device)
|
||||
})
|
||||
if isinstance(message, str):
|
||||
await websocket.send_text(message)
|
||||
else:
|
||||
await websocket.send_json(message)
|
||||
return websocket
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections.values():
|
||||
await connection.send_text(message)
|
||||
|
||||
async def change_status_smart_lock(self, smart_lock: 'SmartLockRead'):
|
||||
# При необходимости ограничить или типизировать отправляемый ответ
|
||||
await self.send_message_to_device(smart_lock, smart_lock.model_dump())
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@websocket_router.websocket("/")
|
||||
async def websocket_endpoint(websocket: WebSocket, token: str, authorize: None):
|
||||
try:
|
||||
device = await manager.connect(websocket, token, authorize)
|
||||
except Exception:
|
||||
await websocket.close()
|
||||
return
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
|
||||
# Debug
|
||||
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authorize</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Authorize</h1>
|
||||
<p>Token:</p>
|
||||
<textarea id="token" rows="4" cols="50"></textarea><br><br>
|
||||
<button onclick="websocketfun()">Send</button>
|
||||
<ul id='messages'>
|
||||
</ul>
|
||||
<script>
|
||||
const websocketfun = () => {
|
||||
let token = document.getElementById("token").value
|
||||
let ws = new WebSocket(`ws://testapi.danserver.keenetic.name/?token=${token}`)
|
||||
ws.onmessage = (event) => {
|
||||
let messages = document.getElementById('messages')
|
||||
let message = document.createElement('li')
|
||||
let content = document.createTextNode(event.data)
|
||||
message.appendChild(content)
|
||||
messages.appendChild(message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@websocket_router.get("/debug_form", tags=["Debug"])
|
||||
async def get():
|
||||
return HTMLResponse(html)
|
@ -1,68 +0,0 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import *
|
||||
from pydantic_extra_types.mac_address import MacAddress
|
||||
|
||||
from .common_schemas import *
|
||||
|
||||
__all__ = ["ModelRead", "DeviceCreate", "DeviceRead", "RoleRead", "RoleCreate",
|
||||
"ActiveDeviceCreate", "ActiveDeviceRead", "DeviceWebsocketAuth", "JwtRefreshClaimsForDeviceRequests",
|
||||
"JwtAccessClaimsForDeviceRequests", "TokensRead", "TokenDataServer"]
|
||||
|
||||
|
||||
class DeviceWebsocketAuth(BaseModel, frozen=True):
|
||||
macaddress: MacAddress
|
||||
last_connection_date: datetime
|
||||
model: Optional[ModelRead] = None
|
||||
|
||||
|
||||
class JwtAccessClaimsForDeviceRequests(BaseModel):
|
||||
device_id: uuid.UUID
|
||||
|
||||
|
||||
class JwtRefreshClaimsForDeviceRequests(BaseModel):
|
||||
part_access: str
|
||||
device_id: uuid.UUID
|
||||
|
||||
|
||||
class TokenDataServer(BaseModel):
|
||||
counter: int
|
||||
hash_counter: str
|
||||
|
||||
@staticmethod
|
||||
def hash(counter: int | str):
|
||||
return binascii.hexlify(hashlib.md5(str(counter).encode()).digest())
|
||||
|
||||
@staticmethod
|
||||
def default():
|
||||
return TokenDataServer(
|
||||
counter=0,
|
||||
hash_counter=TokenDataServer.hash(0)
|
||||
)
|
||||
|
||||
|
||||
class TokensRead(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class DeviceCreate(BaseModel, from_attributes=True):
|
||||
macaddress: MacAddress
|
||||
is_active: bool
|
||||
configuration: Json | dict
|
||||
serial_number: int
|
||||
hub: uuid.UUID | None = None
|
||||
model: Optional[ModelRead] = None
|
||||
|
||||
|
||||
class DeviceRead(DeviceCreate):
|
||||
id: uuid.UUID
|
||||
type_name: str = "Device"
|
||||
hub: uuid.UUID | None = None
|
||||
activated: bool = False
|
||||
active_device: Optional[ActiveDeviceRead] = None
|
||||
last_connection_date: datetime | None = None
|
@ -1 +0,0 @@
|
||||
{"doorlock": {"ip": "192.168.31.222", "host": "21488", "mac": "", "type": "phone"}, "phone": {"ip": "192.168.31.222", "host": "53970", "mac": "1", "type": "controller"}}
|
@ -1,89 +0,0 @@
|
||||
import hashlib
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from device import DeviceRead, Device, TokenDataServer
|
||||
from smart_lock.schemas import SmartLockCreate, SmartLockRead
|
||||
from test_database import *
|
||||
from auth.test_auth import *
|
||||
from smart_lock.test_smart_lock import get_json_for_create
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_devices(auth_client: AsyncClient, session: AsyncSession):
|
||||
# Добавляем устройства всех возможных типов
|
||||
# todo более внятный тест
|
||||
json_smart_lock = get_json_for_create()
|
||||
json_smart_lock_2 = get_json_for_create()
|
||||
json_smart_lock_2["smart_lock"]["hub"]["ip_address"] = "192.168.1.1/32"
|
||||
json_smart_lock_2["smart_lock"]["serial_number"] = 344
|
||||
json_smart_lock_2["smart_lock"]["macaddress"] = "22:11:22:ff:fe:33:44:55"
|
||||
json_smart_lock_2["smart_lock"]["hub"]["serial_number"] = 99
|
||||
|
||||
response = await auth_client.post("/smart_lock/", json=json_smart_lock)
|
||||
response = await auth_client.patch("for_device/activate", params={"token": response.json()["access_token"]})
|
||||
response = await auth_client.post("/smart_lock/", json=json_smart_lock)
|
||||
assert response.json()["activated"] is False
|
||||
response = await auth_client.patch("for_device/activate", params={"token": response.json()["access_token"]})
|
||||
response = await auth_client.post("/smart_lock/", json=json_smart_lock_2)
|
||||
response = await auth_client.patch("for_device/activate", params={"token": response.json()["access_token"]})
|
||||
|
||||
response = await auth_client.get("/device/active_devices")
|
||||
assert response.status_code == 200
|
||||
json = response.json()
|
||||
models: list[DeviceRead] = [DeviceRead.model_validate(i) for i in json]
|
||||
assert len(models) == 2
|
||||
|
||||
# check polymorphism
|
||||
excluded = {'id', 'last_interaction_date', 'active_device', "hub", "last_connection_date", "activated"}
|
||||
json_smart_lock["smart_lock"]["hub"]["id"] = uuid.uuid4()
|
||||
json_smart_lock_2["smart_lock"]["hub"]["id"] = uuid.uuid4()
|
||||
assert (SmartLockRead(**json[0]).model_dump(exclude=excluded) ==
|
||||
SmartLockRead(**json_smart_lock["smart_lock"], id=uuid.uuid4()).model_dump(exclude=excluded))
|
||||
assert (SmartLockRead(**json[1]).model_dump(exclude=excluded) ==
|
||||
SmartLockRead(**json_smart_lock_2["smart_lock"], id=uuid.uuid4()).model_dump(exclude=excluded))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_from_user(auth_client: AsyncClient, session: AsyncSession):
|
||||
device = (await session.execute(select(Device))).scalar()
|
||||
response = await auth_client.get("/device/token_from_user", params={"device_id": device.id})
|
||||
assert response.status_code == 200
|
||||
assert list(response.json().keys()) == ["access_token", "refresh_token"]
|
||||
print(response.json())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token(auth_client: AsyncClient, session: AsyncSession):
|
||||
device = (await session.execute(select(Device))).scalar()
|
||||
response = await auth_client.get("/device/token_from_user", params={"device_id": device.id})
|
||||
assert response.status_code == 200
|
||||
json = response.json()
|
||||
json["counter_hash"] = TokenDataServer.hash(0).decode() # Не было обновений, значит ставим 0
|
||||
|
||||
# 1 обновление токена
|
||||
response = await auth_client.get("/for_device/refresh_token", params=json)
|
||||
assert response.status_code == 200
|
||||
json1 = response.json()
|
||||
|
||||
response = await auth_client.get("/for_device/refresh_token", params=json)
|
||||
assert response.status_code in (401, 403) # Token has been revoked
|
||||
|
||||
# 2 обновление токена со старым счетчиком (не должен сработать)
|
||||
json1["counter_hash"] = json["counter_hash"]
|
||||
response = await auth_client.get("/for_device/refresh_token", params=json1)
|
||||
assert response.status_code in (401, 403) # invalid counter hash
|
||||
|
||||
# 2 обновление токена с обновленным счетчиком
|
||||
json1["counter_hash"] = TokenDataServer.hash(1).decode()
|
||||
response = await auth_client.get("/for_device/refresh_token", params=json1)
|
||||
assert response.status_code == 200
|
||||
|
||||
# 3 обновление токена с обновленным счетчиком
|
||||
# hashlib.md5(str(2).encode()).hexdigest()
|
||||
json2 = response.json()
|
||||
json2["counter_hash"] = TokenDataServer.hash(2).decode()
|
||||
response = await auth_client.get("/for_device/refresh_token", params=json2)
|
||||
assert response.status_code == 200
|
@ -1,12 +0,0 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from .dependencies import jwt_websocket_create_access_token
|
||||
from .router import router
|
||||
from .schemas import DeviceWebsocketAuth
|
||||
|
||||
|
||||
@router.post("/ws/token")
|
||||
def get_access_token_for_connection_to_device(access_token: str = Depends(jwt_websocket_create_access_token)):
|
||||
return {
|
||||
"access_token": access_token
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import abc
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
from fastapi import Depends, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from device.schemas import DeviceWebsocketAuth, TokenDataServer
|
||||
|
||||
# Множество заблокированных токенов доступа по вебсокетам. Токены одноразовые, но при дропе системы список должен
|
||||
# очищаться (не хранится в постоянной памяти), чтобы устройства могли повторно подключиться к вебсокетам после
|
||||
# восстановления
|
||||
# todo перенести на redis
|
||||
denylist = set()
|
||||
tokens_data: dict[str, TokenDataServer] = dict()
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
authjwt_secret_key: str = os.environ.get("SECRET_DEVICE_AUTH_TOKEN")
|
||||
authjwt_denylist_enabled: bool = True
|
||||
authjwt_denylist_token_checks: set = {"access", "refresh"}
|
||||
authjwt_refresh_token_expires: bool = False
|
||||
|
||||
|
||||
def check_if_token_in_denylist(decrypted_token):
|
||||
jti = decrypted_token['jti']
|
||||
return jti in denylist
|
||||
|
||||
|
||||
def get_config():
|
||||
return Settings()
|
||||
|
||||
|
||||
def jwt_websocket_required_auth(token: str = Query(...), authorize: None = Depends()) -> Tuple[dict, 'DeviceWebsocketAuth']:
|
||||
authorize.jwt_required("websocket", token=token)
|
||||
|
||||
decoded_token = authorize.get_raw_jwt(token)
|
||||
device = DeviceWebsocketAuth.model_validate(decoded_token)
|
||||
|
||||
return decoded_token, device
|
@ -1,67 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Depends, Body
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from device.router import router, websocket_router
|
||||
|
||||
app = FastAPI(
|
||||
title="Smart lock",
|
||||
root_path="/devices"
|
||||
)
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html(req: Request):
|
||||
root_path = req.scope.get("root_path", "").rstrip("/")
|
||||
openapi_url = root_path + app.openapi_url
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=openapi_url,
|
||||
title="API",
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
app.include_router(websocket_router)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.perf_counter()
|
||||
response = await call_next(request)
|
||||
process_time = time.perf_counter() - start_time
|
||||
print(f"Finished in {process_time:.3f}")
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
|
||||
|
||||
@app.exception_handler(DBAPIError)
|
||||
def authjwt_exception_handler(request: Request, exc: DBAPIError):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": jsonable_encoder(exc) | {"args": jsonable_encoder(exc.args)}}
|
||||
)
|
||||
|
||||
|
||||
# @app.exception_handler(Exception)
|
||||
# def authjwt_exception_handler(request: Request, exc: Exception):
|
||||
# print(exc.with_traceback(None))
|
||||
# return JSONResponse(
|
||||
# status_code=500,
|
||||
# content={
|
||||
# "name_exception": exc.__class__.__name__,
|
||||
# "args": getattr(exc, "args", []),
|
||||
# "detail": jsonable_encoder(exc)
|
||||
# }
|
||||
# )
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(app=app, host="0.0.0.0", port=8001)
|
||||
# uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)
|
@ -1,9 +0,0 @@
|
||||
fastapi==0.109.1
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
fastapi-users==12.1.2
|
||||
python-dotenv
|
||||
asyncpg
|
||||
pydantic_extra_types
|
||||
fastapi_jwt_auth
|
||||
fastapi_users_db_sqlalchemy
|
@ -1,32 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
user_service:
|
||||
env_file: ".env"
|
||||
build:
|
||||
context: ./user_service
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- PORT=8000
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
device_service:
|
||||
env_file: ".env"
|
||||
build:
|
||||
context: ./device_service
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- PORT=8001
|
||||
ports:
|
||||
- "8001:8001"
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- user_service
|
||||
- device_service
|
@ -1,11 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location /users {
|
||||
proxy_pass http://user_service:8000;
|
||||
}
|
||||
|
||||
location /devices/ {
|
||||
proxy_pass http://device_service:8001;
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
# Лабораторная работа №3: REST API, шлюз и синхронный обмен данными между микросервисами
|
||||
|
||||
## Задание реализовать два микросервиса связанные единой базой и сущностями один ко многим
|
||||
|
||||
- Первый микросервис user предоставляет методы для регистрации авторизации и выхода пользователей
|
||||
- Второй микросервис реализует сущность устройства, которые связанные с пользователями связью многие ко многим
|
||||
|
||||
### Реализация
|
||||
Оба микросервиса написаны на fastapi и развернуты на вебсервере uvicorn. В самом докере развертывание осущетсвляется через веб сервер nginx,
|
||||
который осуществляет роль прокси, прослушивая 80 http порт и перенправляя запрос на тот или иной микросервис
|
||||
|
||||
Для корректной работы swagger-а необходимо перенести location из nginx.conf в fastapi, чтобы ему был известен корневой путь, для построения документации
|
||||
|
||||
`app = FastAPI(
|
||||
title="Smart lock",
|
||||
root_path="/devices"
|
||||
)`
|
||||
|
||||
`location /devices/ {
|
||||
|
||||
proxy_pass http://device_service:8001;
|
||||
|
||||
}`
|
||||
|
||||
### Развертывание
|
||||
В корневую папку добавляем файл .env, где указываем значение переменных среды
|
||||
|
||||
**Пример файла:**
|
||||
|
||||
`DB_HOST = localhost
|
||||
DB_PORT = 5432
|
||||
DB_NAME =
|
||||
DB_USER = postgres
|
||||
DB_PASS = 1234
|
||||
|
||||
SECRET_AUTH_TOKEN = 1
|
||||
SECRET_VERIFICATE_TOKEN = 1
|
||||
SECRET_DEVICE_AUTH_TOKEN = 1
|
||||
|
||||
DEVICE_START_COUNTER = 1
|
||||
|
||||
STATIC_URL = https://testapi.danserver.keenetic.name/static/`
|
||||
|
||||
Далее указываем:
|
||||
|
||||
docker-compose up --build
|
||||
|
||||
[Видео](https://disk.yandex.ru/d/BGenDWYY6tIGaw)
|
@ -1,10 +0,0 @@
|
||||
FROM python:3.11
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["uvicorn", "--host", "0.0.0.0", "main:app", "--port", "8000"]
|
@ -1,6 +0,0 @@
|
||||
from .models import *
|
||||
from .router import *
|
||||
from .schemas import *
|
||||
from .manager import *
|
||||
from .base_config import *
|
||||
from .dependencies import *
|
@ -1,34 +0,0 @@
|
||||
#
|
||||
import uuid
|
||||
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users.authentication import CookieTransport, AuthenticationBackend, BearerTransport
|
||||
from fastapi_users.authentication import JWTStrategy
|
||||
|
||||
from .models import User
|
||||
from .manager import get_user_manager
|
||||
from config import SECRET_AUTH_TOKEN
|
||||
|
||||
__all__ = ["fastapi_users", "current_user", "auth_backend", "current_user_optional"]
|
||||
|
||||
cookie_transport = CookieTransport()
|
||||
bearer_transport = BearerTransport("/token")
|
||||
|
||||
|
||||
def get_jwt_strategy() -> JWTStrategy:
|
||||
return JWTStrategy(secret=SECRET_AUTH_TOKEN, lifetime_seconds=None)
|
||||
|
||||
|
||||
auth_backend = AuthenticationBackend(
|
||||
name="jwt",
|
||||
transport=cookie_transport,
|
||||
get_strategy=get_jwt_strategy,
|
||||
)
|
||||
|
||||
fastapi_users = FastAPIUsers[User, uuid.UUID](
|
||||
get_user_manager,
|
||||
[auth_backend],
|
||||
)
|
||||
|
||||
current_user = fastapi_users.current_user()
|
||||
current_user_optional = fastapi_users.current_user(optional=True)
|
@ -1,13 +0,0 @@
|
||||
#
|
||||
from fastapi import Depends
|
||||
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .models import User
|
||||
from database import get_async_session
|
||||
|
||||
__all__ = ["get_user_db"]
|
||||
|
||||
|
||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||
yield SQLAlchemyUserDatabase(session, User)
|
@ -1,99 +0,0 @@
|
||||
#
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, Request, Response, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi_users import BaseUserManager, UUIDIDMixin, exceptions, models, schemas
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.sql.functions import user
|
||||
|
||||
from database import get_async_session
|
||||
from .dependencies import get_user_db
|
||||
from .models import *
|
||||
from config import SECRET_VERIFICATE_TOKEN as SECRET
|
||||
|
||||
__all__ = ["UserManager", "get_user_manager"]
|
||||
|
||||
from .schemas import UserUpdate
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
reset_password_token_secret = SECRET
|
||||
verification_token_secret = SECRET
|
||||
|
||||
async def on_after_register(self, user: User, request: Optional[Request] = None):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
async def on_after_login(
|
||||
self,
|
||||
user: models.UP,
|
||||
request: Optional[Request] = None,
|
||||
response: Optional[Response] = None,
|
||||
) -> None:
|
||||
if user.fcm_token is not None:
|
||||
await self.update(UserUpdate(fcm_token=user.fcm_token), user, request=request)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
user_create: schemas.UC,
|
||||
safe: bool = False,
|
||||
request: Optional[Request] = None,
|
||||
) -> models.UP:
|
||||
await self.validate_password(user_create.password, user_create)
|
||||
|
||||
existing_user = await self.user_db.get_by_email(user_create.email)
|
||||
if existing_user is not None:
|
||||
raise exceptions.UserAlreadyExists()
|
||||
|
||||
user_dict = (
|
||||
user_create.create_update_dict()
|
||||
if safe
|
||||
else user_create.create_update_dict_superuser()
|
||||
)
|
||||
password = user_dict.pop("password")
|
||||
user_dict["hashed_password"] = self.password_helper.hash(password)
|
||||
|
||||
self._link_or_create_entity(user_dict, "group", Group)
|
||||
|
||||
try:
|
||||
created_user = await self.user_db.create(user_dict)
|
||||
except IntegrityError as e:
|
||||
if 'group' not in e.statement:
|
||||
raise e
|
||||
detail = {
|
||||
"msg": f"Некорректный идентификатор группы {user_dict.get('group_id')=} введенный при регистрации",
|
||||
"inner_exception": jsonable_encoder(e)
|
||||
}
|
||||
raise HTTPException(status_code=403, detail=detail)
|
||||
if hasattr(self.user_db, "session"):
|
||||
await created_user.include_relationship_for_read(self.user_db.session)
|
||||
|
||||
await self.on_after_register(created_user, request)
|
||||
|
||||
return created_user
|
||||
|
||||
async def authenticate(
|
||||
self, credentials: OAuth2PasswordRequestForm
|
||||
) -> Optional[models.UP]:
|
||||
user_auth = await super().authenticate(credentials)
|
||||
if user_auth is None:
|
||||
return None
|
||||
user_auth.fcm_token = credentials.fcm_token
|
||||
return user_auth
|
||||
|
||||
@staticmethod
|
||||
def _link_or_create_entity(user_dict: dict, name: str, EntityType):
|
||||
entity = user_dict.get(name)
|
||||
if isinstance(entity, (uuid.UUID, int)) or entity is None:
|
||||
user_dict.pop(name, None)
|
||||
user_dict[name + "_id"] = entity
|
||||
else:
|
||||
user_dict[name] = EntityType(**entity)
|
||||
|
||||
|
||||
|
||||
|
||||
async def get_user_manager(user_db=Depends(get_user_db)):
|
||||
yield UserManager(user_db)
|
@ -1,52 +0,0 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable
|
||||
from sqlalchemy import Table, Column, Integer, String, TIMESTAMP, ForeignKey, JSON, Boolean, MetaData, DateTime, UUID, \
|
||||
ARRAY, DECIMAL, DOUBLE_PRECISION
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
__all__ = ["Group", "User"]
|
||||
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "groups"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name = Column(String, nullable=False)
|
||||
comment = Column(String, nullable=True)
|
||||
|
||||
users = relationship("User", back_populates="group")
|
||||
|
||||
|
||||
class User(SQLAlchemyBaseUserTable[uuid.UUID], Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
surname = Column(String, nullable=False)
|
||||
patronymic = Column(String, nullable=True)
|
||||
creation_on = Column(TIMESTAMP, default=datetime.utcnow)
|
||||
last_entry = Column(TIMESTAMP, nullable=True)
|
||||
group_id = Column(ForeignKey(Group.id), nullable=True)
|
||||
hashed_password = Column(String(length=1024), nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_superuser = Column(Boolean, default=False, nullable=False)
|
||||
is_verified = Column(Boolean, default=False, nullable=False)
|
||||
token = Column(String, nullable=True)
|
||||
fcm_token = Column(String, nullable=True) # Идентификатор устройства для уведомлений
|
||||
hashes_rfids = Column(ARRAY(String), default=[])
|
||||
face_embedding = Column(ARRAY(DOUBLE_PRECISION), nullable=True)
|
||||
|
||||
devices = relationship("Device", secondary="active_devices", back_populates="users", viewonly=True)
|
||||
active_devices = relationship("ActiveDevice")
|
||||
group = relationship(Group, uselist=False, back_populates="users")
|
||||
notifications = relationship("Notification", back_populates="user")
|
||||
|
||||
async def include_relationship_for_read(self, session: AsyncSession):
|
||||
await session.refresh(self, ["group"])
|
||||
return self
|
@ -1,25 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import get_async_session
|
||||
from . import User
|
||||
from .base_config import current_user
|
||||
from .schemas import UserRead
|
||||
|
||||
__all__ = ["user_router"]
|
||||
|
||||
user_router = APIRouter(prefix='/auth', tags=['Auth'])
|
||||
|
||||
|
||||
@user_router.get("/current_user")
|
||||
async def get_current_user(user=Depends(current_user), session=Depends(get_async_session)) -> UserRead:
|
||||
return await user.include_relationship_for_read(session)
|
||||
|
||||
|
||||
@user_router.patch("/fcm_token/{fcm_token}")
|
||||
async def refresh_fcm_token(fcm_token: str | None = None,
|
||||
user=Depends(current_user), session=Depends(get_async_session)) -> UserRead:
|
||||
if fcm_token is not None:
|
||||
user.fcm_token = fcm_token
|
||||
await session.commit()
|
||||
return user
|
@ -1,39 +0,0 @@
|
||||
#
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
from sqlalchemy import TIMESTAMP
|
||||
from fastapi_users import schemas
|
||||
from pydantic import EmailStr, BaseModel, Json
|
||||
|
||||
__all__ = ["GroupRead", "GroupCreate", "UserRead", "UserCreate", "UserUpdate"]
|
||||
|
||||
|
||||
class GroupCreate(BaseModel):
|
||||
name: str
|
||||
comment: Optional[str]
|
||||
|
||||
|
||||
class GroupRead(GroupCreate, from_attributes=True):
|
||||
id: uuid.UUID
|
||||
|
||||
|
||||
class UserRead(schemas.BaseUser[uuid.UUID], from_attributes=True):
|
||||
name: str
|
||||
surname: str
|
||||
patronymic: Optional[str]
|
||||
creation_on: datetime.datetime
|
||||
fcm_token: Optional[str] = None
|
||||
group: Optional[GroupRead] = None
|
||||
|
||||
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
name: str
|
||||
surname: str
|
||||
patronymic: Optional[str] = None
|
||||
group: Union[None, uuid.UUID, GroupCreate] = None
|
||||
|
||||
|
||||
class UserUpdate(schemas.BaseUserUpdate):
|
||||
fcm_token: Optional[str] = None
|
@ -1,98 +0,0 @@
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import Depends
|
||||
|
||||
from test_database import *
|
||||
from . import User
|
||||
from .schemas import UserCreate, GroupCreate, UserRead
|
||||
|
||||
|
||||
__all__ = ["client", "auth_client", "get_generate_user", "session"]
|
||||
|
||||
|
||||
def _add_password(user: User, password: str):
|
||||
(user if isinstance(user, dict) else user.__dict__)["password"] = password
|
||||
return user
|
||||
|
||||
|
||||
def get_generate_user(unique=False):
|
||||
if not unique:
|
||||
return UserCreate(
|
||||
name="user",
|
||||
surname="test",
|
||||
group=GroupCreate(name="test", comment="test_comment"),
|
||||
email="user@localhost.ru",
|
||||
password="<PASSWORD>"
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def auth_client(app) -> AsyncClient:
|
||||
async with AsyncClient(app=app, base_url="http://127.0.0.1:9000") as client:
|
||||
user = get_generate_user()
|
||||
await client.post("/auth/register", json=user.model_dump())
|
||||
response = await client.post("/auth/login", data={
|
||||
'username': user.email,
|
||||
'password': user.password
|
||||
})
|
||||
cookie = list(dict(response.cookies).items())[-1]
|
||||
client.headers["Cookie"] = "=".join(cookie)
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_user(client, session) -> UserCreate:
|
||||
user = get_generate_user()
|
||||
|
||||
response = await client.post("/auth/register", json=user.model_dump())
|
||||
assert response.status_code == 201, response.json()
|
||||
|
||||
result = response.json()
|
||||
db_user = await (await session.get(User, result["id"])).include_relationship_for_read(session)
|
||||
|
||||
assert db_user is not None
|
||||
assert result.pop("id", None) is not None, response.json()
|
||||
|
||||
compare_model = UserCreate.model_validate(_add_password(result, user.password)).model_dump()
|
||||
db_compare_model = UserCreate.model_validate(_add_password(db_user, user.password), from_attributes=True).model_dump()
|
||||
assert compare_model == db_compare_model
|
||||
assert compare_model == user.model_dump()
|
||||
assert user.model_dump() == db_compare_model
|
||||
response = await client.post("/auth/register", json=user.model_dump())
|
||||
assert response.status_code == 400, response.json()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user(client) -> AsyncClient:
|
||||
user = get_generate_user()
|
||||
await client.post("/auth/register", json=user.model_dump())
|
||||
response = await client.post("/auth/login", data={
|
||||
'username': user.email,
|
||||
'password': user.password + "0"
|
||||
})
|
||||
assert response.status_code == 400, response.json()
|
||||
response = await client.post("/auth/login", data={
|
||||
'username': user.email,
|
||||
'password': user.password
|
||||
})
|
||||
assert response.status_code == 200 or response.status_code == 204, response.json()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authorization_and_logout(auth_client):
|
||||
client: httpx.AsyncClient = auth_client
|
||||
response = await client.get("auth/current_user")
|
||||
assert response.status_code == 200
|
||||
result_user = UserRead(**response.json())
|
||||
sourse_user = get_generate_user()
|
||||
assert result_user.email == sourse_user.email
|
||||
assert result_user.name == sourse_user.name
|
||||
response = await client.post("/auth/logout")
|
||||
assert response.status_code == 200 or response.status_code == 204
|
||||
assert not response.cookies
|
||||
auth_cookies = client.headers.pop("Cookie")
|
||||
response = await client.get("/auth/current_user")
|
||||
assert response.status_code == 401
|
||||
client.headers["Cookie"] = auth_cookies
|
@ -1,21 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DB_HOST = os.environ.get("DB_HOST")
|
||||
DB_PORT = os.environ.get("DB_PORT")
|
||||
DB_NAME = os.environ.get("DB_NAME")
|
||||
DB_USER = os.environ.get("DB_USER")
|
||||
DB_PASS = os.environ.get("DB_PASS")
|
||||
|
||||
|
||||
SECRET_AUTH_TOKEN = os.environ.get("SECRET_AUTH_TOKEN")
|
||||
SECRET_VERIFICATE_TOKEN = os.environ.get("SECRET_VERIFICATE_TOKEN")
|
||||
|
||||
DEVICE_START_COUNTER = os.environ.get("DEVICE_START_COUNTER")
|
||||
|
||||
logging.getLogger('passlib').setLevel(logging.ERROR)
|
||||
|
@ -1,49 +0,0 @@
|
||||
import time
|
||||
from sys import gettrace
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config import DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Table, Column, Integer, String, TIMESTAMP, ForeignKey, Boolean, MetaData, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
def fill_from_model(self, model: BaseModel | dict):
|
||||
"""Заполняет все поля из словаря или модели, помечая объект 'грязным' для корректного обновления"""
|
||||
if isinstance(model, BaseModel):
|
||||
items = model.model_dump().items()
|
||||
else:
|
||||
items = model.items()
|
||||
|
||||
for key, value in items:
|
||||
setattr(self, key, value)
|
||||
|
||||
DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=gettrace() is not None)
|
||||
async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def begin_transactions() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker().begin() as transaction:
|
||||
try:
|
||||
yield transaction.session
|
||||
except Exception as e:
|
||||
await transaction.rollback()
|
||||
raise e
|
||||
await transaction.commit()
|
@ -1,81 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
|
||||
# import firebase_admin
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Depends, Body
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from auth import *
|
||||
|
||||
app = FastAPI(
|
||||
title="Smart lock",
|
||||
root_path="/users"
|
||||
)
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html(req: Request):
|
||||
root_path = req.scope.get("root_path", "").rstrip("/")
|
||||
openapi_url = root_path + app.openapi_url
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=openapi_url,
|
||||
title="API",
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(auth_backend),
|
||||
prefix="/auth",
|
||||
tags=["Auth"],
|
||||
)
|
||||
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(UserRead, UserCreate),
|
||||
prefix="/auth",
|
||||
tags=["Auth"],
|
||||
)
|
||||
|
||||
# firebase
|
||||
# cred = credentials.Certificate("../serviceAccountKey.json")
|
||||
# try:
|
||||
# fire_app = firebase_admin.initialize_app(cred)
|
||||
# except ValueError:
|
||||
# pass
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
msg: str
|
||||
|
||||
|
||||
@app.post("/msg")
|
||||
def debug_message_from_user(msg: Message = Body(), user: User = Depends(current_user_optional)):
|
||||
print(datetime.datetime.now(), end=' ')
|
||||
if user is not None:
|
||||
print(user.__dict__)
|
||||
print(msg.msg)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.perf_counter()
|
||||
response = await call_next(request)
|
||||
process_time = time.perf_counter() - start_time
|
||||
print(f"Finished in {process_time:.3f}")
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
|
||||
|
||||
@app.exception_handler(DBAPIError)
|
||||
def authjwt_exception_handler(request: Request, exc: DBAPIError):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": jsonable_encoder(exc) | {"args": jsonable_encoder(exc.args)}}
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(app=app, host="0.0.0.0", port=8000)
|
@ -1,7 +0,0 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
fastapi-users
|
||||
sqlalchemy
|
||||
fastapi_users_db_sqlalchemy
|
||||
python-dotenv
|
||||
asyncpg
|
@ -1,30 +0,0 @@
|
||||
import pika
|
||||
import time
|
||||
|
||||
|
||||
def callback(ch, method, properties, body):
|
||||
print(f'Consumer 1 получил сообщение: {body.decode()}')
|
||||
|
||||
# Время задержки по условию
|
||||
time.sleep(2)
|
||||
|
||||
print('Consumer 1 закончил обработку')
|
||||
|
||||
|
||||
def consume_events_1():
|
||||
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Создание очереди
|
||||
channel.queue_declare(queue='consumer1_queue')
|
||||
# Привязка очереди
|
||||
channel.queue_bind(exchange='beauty_salon_events', queue='consumer1_queue')
|
||||
|
||||
channel.basic_consume(queue='consumer1_queue', on_message_callback=callback, auto_ack=True)
|
||||
|
||||
print('Consumer 1 начал ожидать сообщения...')
|
||||
channel.start_consuming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
consume_events_1()
|
@ -1,28 +0,0 @@
|
||||
import pika
|
||||
|
||||
|
||||
def callback(ch, method, properties, body):
|
||||
print(f'Consumer 2 получил сообщение: {body.decode()}')
|
||||
|
||||
# Обработка "нон-стопом"
|
||||
print('Consumer 2 закончил обработку')
|
||||
|
||||
|
||||
def consume_events_2():
|
||||
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
|
||||
channel = connection.channel()
|
||||
|
||||
# Создание очереди
|
||||
channel.queue_declare(queue='consumer2_queue')
|
||||
|
||||
# Привязка очереди
|
||||
channel.queue_bind(exchange='beauty_salon_events', queue='consumer2_queue')
|
||||
|
||||
channel.basic_consume(queue='consumer2_queue', on_message_callback=callback, auto_ack=True)
|
||||
|
||||
print('Consumer 2 начал ожидать сообщения...')
|
||||
channel.start_consuming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
consume_events_2()
|
@ -1,51 +0,0 @@
|
||||
# Лабораторная работа №4 - Работа с брокером сообщений
|
||||
|
||||
+ Установить брокер сообщений RabbitMQ.
|
||||
+ Пройти уроки 1, 2 и 3 из RabbitMQ Tutorials на любом языке программирования.
|
||||
+ Продемонстрировать работу брокера сообщений.
|
||||
|
||||
## Описание работы
|
||||
|
||||
**Publisher** - осуществляет отправку сообщений своим клиентам.
|
||||
|
||||
**Consumer1** - принимает и обрабатывает сообщения с задержкой в 2-3 секунды.
|
||||
|
||||
**Consumer2** - моментально принимает и обрабатывает сообщения.
|
||||
|
||||
### Tutorials
|
||||
|
||||
1. tutorial_1
|
||||
|
||||

|
||||
|
||||
2. tutorial_2
|
||||
|
||||

|
||||
|
||||
3. tutorial_3
|
||||
|
||||

|
||||
|
||||
## Работа с RabbitMQ
|
||||
|
||||

|
||||
|
||||
## Показания очереди queue_1 при одном запущенном экземпляре Consumer_1
|
||||
|
||||

|
||||
|
||||
## Показания очереди queue_2
|
||||
|
||||

|
||||
|
||||
## Показания очереди queue_1 при двух запущенных экземплярах Consumer_1
|
||||

|
||||
|
||||
## Показания очереди queue_1 при трех запущенных экземплярах Consumer_1
|
||||
|
||||

|
||||
|
||||
## Видеозапись работы программы
|
||||
|
||||
https://disk.yandex.ru/d/TAdJwo36RrN4ag
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user