Update some work schemes
This commit is contained in:
parent
62e533fec5
commit
d4f18b886f
15 changed files with 568 additions and 924 deletions
92
ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml
Normal file
92
ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
@startuml Crowd Node - Request Processing Flow
|
||||
|
||||
!define COMPONENT_BG_COLOR #E3F2FD
|
||||
!define API_BG_COLOR #FFF3E0
|
||||
!define STORAGE_BG_COLOR #F3E5F5
|
||||
|
||||
title Crowd Node: Процесс обработки запроса на анализ изображения
|
||||
|
||||
participant "Scheduler" as Scheduler
|
||||
participant "Redis Queue" as RedisQueue #FFCCCC
|
||||
participant "tasks.py" as TaskListener COMPONENT_BG_COLOR
|
||||
participant "analyzer.py" as Analyzer COMPONENT_BG_COLOR
|
||||
participant "frames.py" as FramePuller COMPONENT_BG_COLOR
|
||||
participant "LivePreview service" as LivePreview API_BG_COLOR
|
||||
box #LightGreen
|
||||
participant "PersonDetector" as PersonDetector
|
||||
participant "PersonDetectionService" as PersonDetectionService
|
||||
end box
|
||||
participant "Redis Cache" as RedisCache #FFCCCC
|
||||
participant "S3 Storage" as S3 STORAGE_BG_COLOR
|
||||
participant "Central" as Central API_BG_COLOR
|
||||
|
||||
== Получение задания ==
|
||||
|
||||
Scheduler -> RedisQueue: push task\n{cmd: "analyze_crowd",\nparams: {uin, camera_id, zones, ...}}
|
||||
activate RedisQueue
|
||||
|
||||
TaskListener -> RedisQueue: pull_task()
|
||||
activate TaskListener
|
||||
RedisQueue --> TaskListener: task data
|
||||
deactivate RedisQueue
|
||||
|
||||
TaskListener -> Analyzer: analyze_crowd(uin, camera_id, zones, **options)
|
||||
activate Analyzer
|
||||
|
||||
== Получение кадра ==
|
||||
|
||||
Analyzer -> FramePuller: pull(uin, camera)
|
||||
activate FramePuller
|
||||
FramePuller -> LivePreview: GET /internal/preview
|
||||
LivePreview --> FramePuller: JPEG image bytes
|
||||
FramePuller -> FramePuller: Frame(content)\n- создает PIL Image\n- генерирует image_id
|
||||
FramePuller --> Analyzer: Frame object
|
||||
deactivate FramePuller
|
||||
|
||||
group #LightGreen Новый подход
|
||||
== Взаимодействие с новым сервисом==
|
||||
Analyzer -> PersonDetector: _run_detecotrs(uin, camera, frame, zones)
|
||||
activate PersonDetector
|
||||
PersonDetector -> PersonDetector: prepare() - Метод для подготовки\nданных для отправки в сервис
|
||||
PersonDetector -> PersonDetectionService: POST /picture/analyze\nМетод для анализа
|
||||
activate PersonDetectionService
|
||||
PersonDetectionService -> PersonDetectionService: Сервис анализирует изображение
|
||||
PersonDetectionService --> PersonDetector: {results:\n\t[detection_1, detection_2, ...]\n}
|
||||
deactivate PersonDetectionService
|
||||
PersonDetector -> PersonDetector: parse_response() - Метод\nдля приведения резултата в формат,\nкоторый был раньше
|
||||
end
|
||||
PersonDetector --> Analyzer: result dict\n{zone_id: {count, objects}, ...}
|
||||
deactivate PersonDetector
|
||||
|
||||
|
||||
== Форматирование результата ==
|
||||
deactivate Detector
|
||||
|
||||
== Сохранение результата ==
|
||||
|
||||
Analyzer -> S3: storage.upload_fileobj(\n image, bucket, key, ...)
|
||||
S3 --> Analyzer: ObjRef
|
||||
|
||||
Analyzer -> S3: storage.generate_presigned_url(obj_ref)
|
||||
S3 --> Analyzer: presigned_url
|
||||
|
||||
== Отправка результата в Central ==
|
||||
|
||||
Analyzer -> Central: central.send('new_measurement', {\n timestamp,\n camera_id,\n measurement_id,\n image: zones_url,\n timings,\n errors,\n zones: zones_info\n})
|
||||
note right
|
||||
Отправка через aio_broker
|
||||
в очередь 'overmind:input'
|
||||
с командой 'new_measurement'
|
||||
end note
|
||||
Central --> Analyzer: (async, no wait)
|
||||
|
||||
Analyzer -> Analyzer: Обновить БД zones_db:\ndetected_at = time.time()
|
||||
|
||||
Analyzer --> TaskListener: complete
|
||||
deactivate Analyzer
|
||||
|
||||
TaskListener -> TaskListener: Ожидать следующую задачу
|
||||
deactivate TaskListener
|
||||
|
||||
@enduml
|
||||
|
||||
249
ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml
Normal file
249
ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
@startuml Crowd Node - Request Processing Flow
|
||||
|
||||
!define COMPONENT_BG_COLOR #E3F2FD
|
||||
!define API_BG_COLOR #FFF3E0
|
||||
!define STORAGE_BG_COLOR #F3E5F5
|
||||
|
||||
title Crowd Node: Процесс обработки запроса на анализ изображения
|
||||
|
||||
participant "Scheduler" as Scheduler
|
||||
participant "Redis Queue" as RedisQueue #FFCCCC
|
||||
participant "tasks.py\n(Task Listener)" as TaskListener COMPONENT_BG_COLOR
|
||||
participant "analyzer.py\n(Main Analyzer)" as Analyzer COMPONENT_BG_COLOR
|
||||
participant "frames.py\n(Frame Puller)" as FramePuller COMPONENT_BG_COLOR
|
||||
participant "LivePreview service" as LivePreview API_BG_COLOR
|
||||
participant "TevianHeadsDetector\n(detectors/tevian.py)" as Detector COMPONENT_BG_COLOR
|
||||
participant "Redis Cache" as RedisCache #FFCCCC
|
||||
participant "Tevian Cloud API\n(ext_api/tevian_api.py)" as TevianAPI API_BG_COLOR
|
||||
participant "S3 Storage" as S3 STORAGE_BG_COLOR
|
||||
participant "Central (crowd backend)" as Central API_BG_COLOR
|
||||
|
||||
== Получение задания ==
|
||||
|
||||
Scheduler -> RedisQueue: push task\n{cmd: "analyze_crowd",\nparams: {uin, camera_id, zones, ...}}
|
||||
activate RedisQueue
|
||||
|
||||
TaskListener -> RedisQueue: pull_task()
|
||||
activate TaskListener
|
||||
RedisQueue --> TaskListener: task data
|
||||
deactivate RedisQueue
|
||||
|
||||
TaskListener -> Analyzer: analyze_crowd(uin, camera_id, zones, **options)
|
||||
activate Analyzer
|
||||
|
||||
== Получение кадра ==
|
||||
|
||||
Analyzer -> FramePuller: pull(uin, camera)
|
||||
activate FramePuller
|
||||
FramePuller -> LivePreview: GET /internal/preview
|
||||
activate LivePreview
|
||||
LivePreview --> FramePuller: JPEG image bytes
|
||||
deactivate LivePreview
|
||||
FramePuller -> FramePuller: Frame(content)\n- создает PIL Image\n- генерирует image_id
|
||||
FramePuller --> Analyzer: Frame object
|
||||
deactivate FramePuller
|
||||
|
||||
== Запуск детектора ==
|
||||
|
||||
Analyzer -> Analyzer: _run_detectors(uin, camera, frame, zones)
|
||||
Analyzer -> Detector: request(uin, camera, frame, zones)
|
||||
activate Detector
|
||||
|
||||
== Подготовка Tevian (prepare) ==
|
||||
|
||||
Detector -> Detector: prepare(cam_name, zones)
|
||||
note right
|
||||
Подготовка включает:
|
||||
1. Создание/получение камеры
|
||||
2. Синхронизацию очередей (зон)
|
||||
3. Обновление параметров зон
|
||||
end note
|
||||
|
||||
Detector -> Detector: _get_or_create_camera(cam_name)
|
||||
|
||||
Detector -> RedisCache: get_camera(cam_name)
|
||||
activate RedisCache
|
||||
RedisCache --> Detector: TCamera or None
|
||||
deactivate RedisCache
|
||||
|
||||
alt Камеры нет в кеше
|
||||
Detector -> TevianAPI: TCamera.get_all()
|
||||
activate TevianAPI
|
||||
TevianAPI -> TevianAPI: _refresh_token() if needed
|
||||
TevianAPI -> "Tevian Cloud": GET /api/cameras
|
||||
"Tevian Cloud" --> TevianAPI: список камер [{id, name, ...}]
|
||||
TevianAPI --> Detector: [TCamera, ...]
|
||||
deactivate TevianAPI
|
||||
|
||||
Detector -> RedisCache: set_camera(cam) для каждой
|
||||
|
||||
alt Камера все еще не найдена
|
||||
Detector -> TevianAPI: TCamera.create(cam_name)
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": POST /api/cameras\n{name, rtsp, frequency_plan_id, ...}
|
||||
"Tevian Cloud" --> TevianAPI: {id, name, status, ...}
|
||||
TevianAPI --> Detector: TCamera
|
||||
deactivate TevianAPI
|
||||
Detector -> RedisCache: set_camera(cam)
|
||||
end
|
||||
end
|
||||
|
||||
== Синхронизация очередей (зон) ==
|
||||
|
||||
Detector -> Detector: _get_camera_queues(cam)
|
||||
Detector -> RedisCache: get_queue(q_id)\nдля каждого queues_ids камеры
|
||||
RedisCache --> Detector: TQueue objects
|
||||
|
||||
loop Для каждой зоны из запроса
|
||||
Detector -> Detector: Конвертировать координаты\nв относительные (0..1)
|
||||
|
||||
alt Очередь не найдена
|
||||
Detector -> TevianAPI: TQueue.create(cam_id, zone_id, polygon, min_head_size)
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": POST /api/queues\n{name, camera_id, roi_polygon_relative, ...}
|
||||
"Tevian Cloud" --> TevianAPI: {id, name, camera_id, ...}
|
||||
TevianAPI --> Detector: TQueue
|
||||
deactivate TevianAPI
|
||||
Detector -> RedisCache: set_queue(queue)
|
||||
else Параметры зоны изменились
|
||||
Detector -> TevianAPI: queue.save()
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": POST /api/queues/{id}\n{roi_polygon_relative, ...}
|
||||
"Tevian Cloud" --> TevianAPI: updated queue
|
||||
TevianAPI --> Detector: success
|
||||
deactivate TevianAPI
|
||||
Detector -> RedisCache: set_queue(queue)
|
||||
end
|
||||
end
|
||||
|
||||
loop Для старых очередей (не в списке зон)
|
||||
Detector -> TevianAPI: TQueue.delete_by_id(queue_id)
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": DELETE /api/queues/{id}
|
||||
"Tevian Cloud" --> TevianAPI: success
|
||||
TevianAPI --> Detector: success
|
||||
deactivate TevianAPI
|
||||
Detector -> RedisCache: delete_queue(queue_id)
|
||||
end
|
||||
|
||||
Detector -> TevianAPI: cam.refresh()
|
||||
note right
|
||||
Обновляем состояние камеры
|
||||
после изменения очередей
|
||||
end note
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": GET /api/cameras/{id}
|
||||
"Tevian Cloud" --> TevianAPI: {status, is_accepting_snapshots, ...}
|
||||
TevianAPI --> Detector: updated TCamera
|
||||
deactivate TevianAPI
|
||||
|
||||
Detector -> RedisCache: set_camera(cam)
|
||||
|
||||
== Отправка снапшота и получение результатов ==
|
||||
|
||||
Detector -> Detector: Проверка rate limiting\n(FORCED_WAIT_PERIOD)
|
||||
note right
|
||||
Избегаем HTTP 429: Too Many Requests
|
||||
Ждем если запрос слишком частый
|
||||
end note
|
||||
|
||||
alt Слишком частые запросы
|
||||
Detector -> Detector: asyncio.sleep(wait_for)
|
||||
end
|
||||
|
||||
Detector -> TevianAPI: cam.send_snapshot(frame.data)
|
||||
activate TevianAPI
|
||||
TevianAPI -> "Tevian Cloud": POST /api/cameras/{id}/snapshots\nContent-Type: image/jpeg\nbody: <JPEG bytes>
|
||||
"Tevian Cloud" --> TevianAPI: {snapshot_accepted_at: <timestamp>}
|
||||
TevianAPI --> Detector: timestamp
|
||||
deactivate TevianAPI
|
||||
|
||||
Detector -> Detector: asyncio.sleep(TEVIAN_RECOGNITION_DELAY)\n(рекомендуется 12 сек)
|
||||
|
||||
Detector -> TevianAPI: TRecognition.get_many(queues_ids, timestamp)
|
||||
activate TevianAPI
|
||||
|
||||
loop Polling до получения результатов или timeout
|
||||
TevianAPI -> "Tevian Cloud": GET /api/recognitions?\nqueues_ids={ids}&utc_timestamp={ts}
|
||||
"Tevian Cloud" --> TevianAPI: [recognitions...]
|
||||
|
||||
alt Результатов меньше чем очередей
|
||||
TevianAPI -> TevianAPI: await gen.sleep(2)\nи повторить
|
||||
else Все результаты получены
|
||||
TevianAPI -> TevianAPI: break
|
||||
end
|
||||
end
|
||||
|
||||
TevianAPI -> TevianAPI: Фильтровать detections:\nоставить только\nfiltered_status == 'passed_filters'
|
||||
TevianAPI --> Detector: [TRecognition, ...]
|
||||
deactivate TevianAPI
|
||||
|
||||
== Форматирование результата ==
|
||||
|
||||
loop Для каждого recognition
|
||||
Detector -> RedisCache: get_queue(rec.queue_id)
|
||||
RedisCache --> Detector: TQueue
|
||||
|
||||
Detector -> Detector: Форматировать objects:\n[{x, y, w, h}, ...]\nиз bbox данных
|
||||
|
||||
Detector -> Detector: result[queue.name] = {\n 'count': len(objects),\n 'objects': objects\n}
|
||||
end
|
||||
|
||||
Detector --> Analyzer: result dict\n{zone_id: {count, objects}, ...}
|
||||
deactivate Detector
|
||||
|
||||
Analyzer -> Analyzer: _build_zones_info(zones, detected_values)
|
||||
note right
|
||||
Объединяет данные зон с результатами
|
||||
детекторов, определяет length_by_ai
|
||||
end note
|
||||
|
||||
Analyzer -> Analyzer: _get_triggered_zones(zones_info, timestamp)
|
||||
note right
|
||||
Определяет зоны для подсветки
|
||||
на основе trigger_at и trigger_type
|
||||
end note
|
||||
|
||||
== Сохранение результата ==
|
||||
|
||||
Analyzer -> Analyzer: frame.draw_zones(triggered_zones)
|
||||
note right
|
||||
Рисует полигоны триггерных зон
|
||||
на изображении с прозрачностью
|
||||
end note
|
||||
|
||||
Analyzer -> Analyzer: resize_image(image, 640)
|
||||
|
||||
Analyzer -> S3: storage.upload_fileobj(\n image, bucket, key, ...)
|
||||
activate S3
|
||||
S3 --> Analyzer: ObjRef
|
||||
deactivate S3
|
||||
|
||||
Analyzer -> S3: storage.generate_presigned_url(obj_ref)
|
||||
activate S3
|
||||
S3 --> Analyzer: presigned_url
|
||||
deactivate S3
|
||||
|
||||
Analyzer -> Analyzer: Удалить query params\nиз URL (сделать публичным)
|
||||
|
||||
== Отправка результата в Central ==
|
||||
|
||||
Analyzer -> Central: central.send('new_measurement', {\n timestamp,\n camera_id,\n measurement_id,\n image: zones_url,\n timings,\n errors,\n zones: zones_info\n})
|
||||
activate Central
|
||||
note right
|
||||
Отправка через aio_broker
|
||||
в очередь 'overmind:input'
|
||||
с командой 'new_measurement'
|
||||
end note
|
||||
Central --> Analyzer: (async, no wait)
|
||||
deactivate Central
|
||||
|
||||
Analyzer -> Analyzer: Обновить БД zones_db:\ndetected_at = time.time()
|
||||
|
||||
Analyzer --> TaskListener: complete
|
||||
deactivate Analyzer
|
||||
|
||||
TaskListener -> TaskListener: Ожидать следующую задачу
|
||||
deactivate TaskListener
|
||||
|
||||
@enduml
|
||||
59
ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml
Normal file
59
ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@startuml Crowd Node - Request Processing Flow
|
||||
|
||||
!define COMPONENT_BG_COLOR #E3F2FD
|
||||
!define API_BG_COLOR #FFF3E0
|
||||
!define STORAGE_BG_COLOR #F3E5F5
|
||||
|
||||
participant "tasks.py" as TaskListener COMPONENT_BG_COLOR
|
||||
participant "analyzer.py" as Analyzer COMPONENT_BG_COLOR
|
||||
participant "PersonDetector" as PersonDetector
|
||||
participant "PersonDetectionService" as PersonDetectionService
|
||||
participant "Redis Queue" as RedisQueue #FFCCCC
|
||||
participant "S3 Storage" as S3 STORAGE_BG_COLOR
|
||||
participant "Central" as Central API_BG_COLOR
|
||||
|
||||
== Вариант 1: Получаем задачу в analyze, процессим синхронно и отдаем ответ ==
|
||||
TaskListener -> Analyzer : Получена задача на процессинг
|
||||
Analyzer -> PersonDetector : Отправка задачи на анализ
|
||||
PersonDetector -> PersonDetectionService : Установка HTTP соединения
|
||||
group Открытое HTTP соединение
|
||||
PersonDetector -> PersonDetectionService : HTTP POST запрос
|
||||
PersonDetectionService -> PersonDetectionService : Обарботка запроса
|
||||
PersonDetectionService --> PersonDetector : Отправка в response результата
|
||||
end group
|
||||
PersonDetector --> Analyzer : Результаты задачи
|
||||
== Вариант 2: Получаем задачу в analyze, кладем в очередь, процессим в очереди, формируем результат и отдаем в ответе analyze ==
|
||||
TaskListener -> Analyzer : Получена задача на процессинг
|
||||
Analyzer -> RedisQueue : Положили в очередь задачу на анализ изображения
|
||||
group Polling
|
||||
PersonDetector -> RedisQueue : полит очередь на предмет наличия задач
|
||||
RedisQueue --> PersonDetector : Получает задачу на анализ
|
||||
group Асинхронный запрос на анализ
|
||||
PersonDetector -> PersonDetectionService : Дерганье API сервиса для анализа
|
||||
PersonDetectionService -> PersonDetectionService : Обарботка запроса
|
||||
PersonDetectionService --> PersonDetector : Отправка в response результата
|
||||
end group
|
||||
end group
|
||||
|
||||
group Polling
|
||||
Analyzer -> RedisQueue : Полит в ожидании выполненных задач
|
||||
RedisQueue --> Analyzer : Выполненные задачи анализа
|
||||
end group
|
||||
== Вариант 3: Получаем задачу в analyze, процессим синхронно, формируем результат, отдаем в ответе get_results ==
|
||||
note over PersonDetector
|
||||
Такое ощущение что это похоже не вариант 1
|
||||
end note
|
||||
== Вариант 4: Получаем задачу в analyze кладем в очередь, процессим в очереди, формируем результат и отдаем в ответе get_results ==
|
||||
TaskListener -> Analyzer : Получена задача на процессинг
|
||||
activate Analyzer
|
||||
Analyzer -> PersonDetector : Отправка задачи на анализ\n<b>во внутреннюю очередь PersonDetector'a</b>
|
||||
group Внутрянка PersonDetector'a
|
||||
PersonDetector -> PersonDetector: как то хэндлит запросы на обработку
|
||||
group Обработка в порядке очереди
|
||||
PersonDetector -> PersonDetectionService : отправляет HTTP запросы на обработку\nпо внутренней логике
|
||||
PersonDetectionService --> PersonDetector
|
||||
PersonDetector --> Analyzer : Результат задачи на обработку
|
||||
deactivate Analyzer
|
||||
end group
|
||||
end group
|
||||
@enduml
|
||||
|
|
@ -1,540 +0,0 @@
|
|||
@startuml Folder System Architecture
|
||||
|
||||
title Система папок/групп в Ivideon
|
||||
|
||||
package "Database" @startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@startuml FolderSystemSimple
|
||||
|
||||
title Система папок в Ivideon
|
||||
|
||||
entity "folders" as folders_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* name : string
|
||||
* parents : array
|
||||
* objects : array
|
||||
* root : boolean
|
||||
}
|
||||
|
||||
entity "permission_grants" as grants_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* object_id : string
|
||||
* object_type : string
|
||||
* grantee_id : string
|
||||
* permissions : array
|
||||
}
|
||||
|
||||
entity "servers" as servers_db {
|
||||
* _id : ObjectId
|
||||
--
|
||||
* owner_id : string
|
||||
* cameras : object
|
||||
}
|
||||
|
||||
entity "Folder" as folder_class {
|
||||
+ get_objects(type)
|
||||
+ add_object(obj)
|
||||
+ remove_object(obj)
|
||||
+ has_permissions(perm)
|
||||
}
|
||||
|
||||
entity "FolderTree" as tree_class {
|
||||
+ folders : dict
|
||||
+ find_folders()
|
||||
+ reload()
|
||||
}
|
||||
|
||||
entity "Camera" as camera_node {
|
||||
+ id : "server:index"
|
||||
+ object_type : "camera"
|
||||
}
|
||||
|
||||
folders_db ||--o{ folder_class
|
||||
grants_db ||--o{ folder_class
|
||||
servers_db ||--o{ camera_node
|
||||
|
||||
tree_class --> folder_class : manages
|
||||
folder_class --> camera_node : contains
|
||||
|
||||
note right of folders_db
|
||||
objects[] format:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note bottom of tree_class
|
||||
Usage:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
@enduml
|
||||
{
|
||||
database folders_db as "folders collection"
|
||||
database grants_db as "permission_grants"
|
||||
database servers_db as "servers collection"
|
||||
}
|
||||
|
||||
package "Folder Classes" {
|
||||
class Folder {
|
||||
+id: ObjectId
|
||||
+owner_id: string
|
||||
+name: string
|
||||
+parents: List[string]
|
||||
+objects: List[dict]
|
||||
+root: boolean
|
||||
--
|
||||
+get_objects(type): List[string]
|
||||
+add_object(obj)
|
||||
+remove_object(obj)
|
||||
+has_permissions(perm): boolean
|
||||
}
|
||||
|
||||
class FolderTree {
|
||||
+owner_id: string
|
||||
+folders: Dict[id, Folder]
|
||||
+objects: Dict[type, Dict[id, Node]]
|
||||
+roots: List[Folder]
|
||||
--
|
||||
+find_folders(): List[Folder]
|
||||
+reload()
|
||||
}
|
||||
|
||||
class BaseNode {
|
||||
+id: string
|
||||
+owner_id: string
|
||||
+grantee_id: string
|
||||
+grants: Set[PermissionGrant]
|
||||
--
|
||||
+has_permissions(perm): boolean
|
||||
+permissions: Tuple[string]
|
||||
}
|
||||
}
|
||||
|
||||
package "Permission System" {
|
||||
class PermissionGrant {
|
||||
+object_id: string
|
||||
+object_type: string
|
||||
+grantee_id: string
|
||||
+permissions: List[string]
|
||||
+shared_at: dict
|
||||
}
|
||||
}
|
||||
|
||||
package "Node Types" {
|
||||
class Camera {
|
||||
+id: "server:index"
|
||||
+object_type: "camera"
|
||||
}
|
||||
}
|
||||
|
||||
' Relationships
|
||||
Folder --|> BaseNode
|
||||
FolderTree --> Folder : manages
|
||||
Folder --> PermissionGrant : has grants
|
||||
BaseNode --> PermissionGrant : uses
|
||||
|
||||
Folder --> folders_db : stored in
|
||||
PermissionGrant --> grants_db : stored in
|
||||
Camera --> servers_db : stored in
|
||||
|
||||
' Composition relationships
|
||||
Folder --> Camera : "contains (objects[])"
|
||||
Folder --> Folder : "contains subfolders"
|
||||
|
||||
note right of Folder
|
||||
objects[] содержит:
|
||||
[
|
||||
{object_type: "camera",
|
||||
object_id: "server:0"},
|
||||
{object_type: "folder",
|
||||
object_id: "subfolder_id"}
|
||||
]
|
||||
end note
|
||||
|
||||
note right of FolderTree
|
||||
Главная точка доступа:
|
||||
tree = FolderTree(user_id)
|
||||
folder = tree.folders[folder_id]
|
||||
cameras = folder.get_objects("camera")
|
||||
end note
|
||||
|
||||
note left of PermissionGrant
|
||||
Права доступа:
|
||||
- admin (изменение)
|
||||
- read (просмотр)
|
||||
- Наследование по иерархии
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
@startuml _get_all_user_cameras Sequence Diagram
|
||||
|
||||
title Последовательность выполнения _get_all_user_cameras
|
||||
|
||||
participant "Caller" as caller
|
||||
participant "_get_all_user_cameras" as main_func
|
||||
participant "_get_servers" as get_servers
|
||||
participant "MongoDB" as mongo
|
||||
participant "ivideon.servers" as servers_collection
|
||||
|
||||
note over main_func
|
||||
Входные параметры:
|
||||
- user_id: int
|
||||
- requested_cameras: list[str]
|
||||
(формат: ["server1:0", "server1:1"])
|
||||
- service_name: str (например: "crowd")
|
||||
end note
|
||||
|
||||
caller -> main_func: _get_all_user_cameras(user_id, requested_cameras, service_name)
|
||||
activate main_func
|
||||
|
||||
main_func -> main_func: cameras = {}
|
||||
|
||||
main_func -> get_servers: _get_servers(requested_cameras)
|
||||
activate get_servers
|
||||
|
||||
note over get_servers
|
||||
Извлекает server_ids из camera_ids:
|
||||
["server1:0", "server1:1"]
|
||||
→ ["server1", "server1"]
|
||||
→ ["server1"]
|
||||
end note
|
||||
@startuml _get_all_user_cameras Activity Diagram
|
||||
|
||||
title Алгоритм работы _get_all_user_cameras
|
||||
|
||||
start
|
||||
|
||||
note right
|
||||
**Входные параметры:**
|
||||
• user_id: int
|
||||
• requested_cameras: list[str]
|
||||
(формат: ["server1:0", "server1:1"])
|
||||
• service_name: str (например: "crowd")
|
||||
end note
|
||||
|
||||
:Инициализация cameras = {};
|
||||
|
||||
:Извлечь server_ids из requested_cameras|
|
||||
note right
|
||||
["server1:0", "server1:1"]
|
||||
→ ["server1"]
|
||||
end note
|
||||
|
||||
:Построить MongoDB запрос:
|
||||
query = {
|
||||
'deleted': {'$ne': True},
|
||||
'_id': {'$in': server_ids}
|
||||
}|
|
||||
|
||||
:Задать проекцию полей:
|
||||
projection = {
|
||||
'_id': 1, 'owner_id': 1, 'name': 1,
|
||||
'cameras': 1, 'cam_services': 1,
|
||||
'info': 1, 'timezone': 1
|
||||
}|
|
||||
|
||||
:Выполнить запрос к MongoDB:
|
||||
servers = db.ivideon().servers.find(query, projection)|
|
||||
|
||||
partition "Обработка серверов" {
|
||||
:Взять следующий server;
|
||||
|
||||
while (Есть серверы для обработки?) is (да)
|
||||
:server_id = server['_id'];
|
||||
:is_shared = server['owner_id'] != user_id;
|
||||
:server_build_type = server.get('info', {}).get('build_type', '');
|
||||
:is_server_embedded = server_build_type.endswith('camera');
|
||||
:cam_services = server.get('cam_services', {});
|
||||
|
||||
partition "Обработка камер сервера" {
|
||||
:Взять следующую камеру (camera_idx, camera_data);
|
||||
|
||||
while (Есть камеры на сервере?) is (да)
|
||||
:service_info = cam_services.get(camera_idx, {})
|
||||
.get(service_name, {});
|
||||
|
||||
if (service_info.get('active', False) == True?) then (да)
|
||||
:camera_id = f'{server_id}:{camera_idx}';
|
||||
|
||||
if (is_server_embedded?) then (да)
|
||||
:camera_name = server['name'];
|
||||
else (нет)
|
||||
:camera_name = camera_data.get('name');
|
||||
endif
|
||||
|
||||
:cameras[camera_id] = {
|
||||
'id': camera_id,
|
||||
'owner_id': server['owner_id'],
|
||||
'server': server_id,
|
||||
'name': camera_name,
|
||||
'is_shared': is_shared,
|
||||
'timezone': server.get('timezone') or
|
||||
server.get('timezone_default'),
|
||||
'is_embedded': is_server_embedded
|
||||
};
|
||||
|
||||
else (нет)
|
||||
note right: Камера пропускается - сервис неактивен
|
||||
endif
|
||||
|
||||
:Взять следующую камеру (camera_idx, camera_data);
|
||||
endwhile (нет)
|
||||
}
|
||||
|
||||
:Взять следующий server;
|
||||
endwhile (нет)
|
||||
}
|
||||
|
||||
:return cameras;
|
||||
|
||||
stop
|
||||
|
||||
note left
|
||||
**Результат:** dict[camera_id, camera_info]
|
||||
|
||||
**Пример:**
|
||||
{
|
||||
"507f...439011:0": {
|
||||
"id": "507f...439011:0",
|
||||
"owner_id": "user123",
|
||||
"server": "507f...439011",
|
||||
"name": "Камера входа",
|
||||
"is_shared": false,
|
||||
"timezone": "Europe/Moscow",
|
||||
"is_embedded": false
|
||||
}
|
||||
}
|
||||
end note
|
||||
|
||||
@enduml
|
||||
ggVG
|
||||
get_servers -> get_servers: requested_server_ids = [camera_id.split(':')[0] \\nfor camera_id in requested_camera_ids]
|
||||
|
||||
get_servers -> get_servers: query = {\n 'deleted': {'$ne': True},\n '_id': {'$in': requested_server_ids}\n}
|
||||
|
||||
get_servers -> get_servers: projection = {\n '_id': 1, 'owner_id': 1, 'name': 1,\n 'cameras': 1, 'cam_services': 1,\n 'info': 1, 'timezone': 1\n}
|
||||
|
||||
get_servers -> mongo: db.ivideon().servers.find(query, projection)
|
||||
activate mongo
|
||||
mongo -> servers_collection: find documents
|
||||
activate servers_collection
|
||||
servers_collection -> mongo: return server documents
|
||||
deactivate servers_collection
|
||||
mongo -> get_servers: list[server_documents]
|
||||
deactivate mongo
|
||||
|
||||
get_servers -> main_func: return servers_list
|
||||
deactivate get_servers
|
||||
|
||||
loop for each server in servers_list
|
||||
main_func -> main_func: server@startuml _get_all_user_cameras Activity Diagram
|
||||
|
||||
title Алгоритм работы _get_all_user_cameras
|
||||
|
||||
start
|
||||
|
||||
note right
|
||||
**Входные параметры:**
|
||||
• user_id: int
|
||||
• requested_cameras: list[str]
|
||||
(формат: ["server1:0", "server1:1"])
|
||||
• service_name: str (например: "crowd")
|
||||
end note
|
||||
|
||||
:Инициализация cameras = {};
|
||||
|
||||
:Извлечь server_ids из requested_cameras|
|
||||
note right
|
||||
["server1:0", "server1:1"]
|
||||
→ ["server1"]
|
||||
end note
|
||||
|
||||
:Построить MongoDB запрос:
|
||||
query = {
|
||||
'deleted': {'$ne': True},
|
||||
'_id': {'$in': server_ids}
|
||||
}|
|
||||
|
||||
:Задать проекцию полей:
|
||||
projection = {
|
||||
'_id': 1, 'owner_id': 1, 'name': 1,
|
||||
'cameras': 1, 'cam_services': 1,
|
||||
'info': 1, 'timezone': 1
|
||||
}|
|
||||
|
||||
:Выполнить запрос к MongoDB:
|
||||
servers = db.ivideon().servers.find(query, projection)|
|
||||
|
||||
partition "Обработка серверов" {
|
||||
:Взять следующий server;
|
||||
|
||||
while (Есть серверы для обработки?) is (да)
|
||||
:server_id = server['_id'];
|
||||
:is_shared = server['owner_id'] != user_id;
|
||||
:server_build_type = server.get('info', {}).get('build_type', '');
|
||||
:is_server_embedded = server_build_type.endswith('camera');
|
||||
:cam_services = server.get('cam_services', {});
|
||||
|
||||
partition "Обработка камер сервера" {
|
||||
:Взять следующую камеру (camera_idx, camera_data);
|
||||
|
||||
while (Есть камеры на сервере?) is (да)
|
||||
:service_info = cam_services.get(camera_idx, {})
|
||||
.get(service_name, {});
|
||||
|
||||
if (service_info.get('active', False) == True?) then (да)
|
||||
:camera_id = f'{server_id}:{camera_idx}';
|
||||
|
||||
if (is_server_embedded?) then (да)
|
||||
:camera_name = server['name'];
|
||||
else (нет)
|
||||
:camera_name = camera_data.get('name');
|
||||
endif
|
||||
|
||||
:cameras[camera_id] = {
|
||||
'id': camera_id,
|
||||
'owner_id': server['owner_id'],
|
||||
'server': server_id,
|
||||
'name': camera_name,
|
||||
'is_shared': is_shared,
|
||||
'timezone': server.get('timezone') or
|
||||
server.get('timezone_default'),
|
||||
'is_embedded': is_server_embedded
|
||||
};
|
||||
|
||||
else (нет)
|
||||
note right: Камера пропускается - сервис неактивен
|
||||
endif
|
||||
|
||||
:Взять следующую камеру (camera_idx, camera_data);
|
||||
endwhile (нет)
|
||||
}
|
||||
|
||||
:Взять следующий server;
|
||||
endwhile (нет)
|
||||
}
|
||||
|
||||
:return cameras;
|
||||
|
||||
stop
|
||||
|
||||
note left
|
||||
**Результат:** dict[camera_id, camera_info]
|
||||
|
||||
**Пример:**
|
||||
{
|
||||
"507f...439011:0": {
|
||||
"id": "507f...439011:0",
|
||||
"owner_id": "user123",
|
||||
"server": "507f...439011",
|
||||
"name": "Камера входа",
|
||||
"is_shared": false,
|
||||
"timezone": "Europe/Moscow",
|
||||
"is_embedded": false
|
||||
}
|
||||
}
|
||||
end note
|
||||
|
||||
@enduml
|
||||
ggVG_id = server['_id']
|
||||
main_func -> main_func: is_shared = server['owner_id'] != user_id
|
||||
main_func -> main_func: server_build_type = server.get('info', {}).get('build_type', '')
|
||||
main_func -> main_func: is_server_embedded = server_build_type.endswith('camera')
|
||||
main_func -> main_func: cam_services = server.get('cam_services', {})
|
||||
|
||||
loop for camera_idx, camera_data in server.cameras.items()
|
||||
main_func -> main_func: service_info = cam_services.get(camera_idx, {})\\n .get(service_name, {})
|
||||
|
||||
alt service_info.get('active', False) == True
|
||||
main_func -> main_func: camera_id = f'{server_id}:{camera_idx}'
|
||||
|
||||
alt is_server_embedded == True
|
||||
main_func -> main_func: camera_name = server['name']
|
||||
else
|
||||
main_func -> main_func: camera_name = camera_data.get('name')
|
||||
end
|
||||
|
||||
main_func -> main_func: cameras[camera_id] = {\n 'id': camera_id,\n 'owner_id': server['owner_id'],\n 'server': server_id,\n 'name': camera_name,\n 'is_shared': is_shared,\n 'timezone': server.timezone,\n 'is_embedded': is_server_embedded\n}
|
||||
|
||||
note right
|
||||
Создается полная информация
|
||||
о камере для возврата
|
||||
end note
|
||||
|
||||
else
|
||||
note right
|
||||
Камера пропускается:
|
||||
сервис неактивен
|
||||
end note
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
main_func -> caller: return cameras dict
|
||||
deactivate main_func
|
||||
|
||||
note over caller
|
||||
Результат: dict[camera_id, camera_info]
|
||||
где camera_id = "server_id:camera_index"
|
||||
|
||||
Пример:
|
||||
{
|
||||
"507f...439011:0": {
|
||||
"id": "507f...439011:0",
|
||||
"owner_id": "user123",
|
||||
"server": "507f...439011",
|
||||
"name": "Камера входа",
|
||||
"is_shared": false,
|
||||
"timezone": "Europe/Moscow",
|
||||
"is_embedded": false
|
||||
}
|
||||
}
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
@startuml
|
||||
title Верхнеуровневые сущности сервиса Crowd
|
||||
|
||||
card api_concept{
|
||||
entity "CrowdReport(APIObject)" as CRA{
|
||||
+ id str
|
||||
+ owner_id str
|
||||
+ type str
|
||||
+ name str
|
||||
+ status str
|
||||
+ created_at timestamp
|
||||
+ updated_at timestamp
|
||||
+ progress int
|
||||
+ options dict
|
||||
+ create() -> CrowdReport
|
||||
}
|
||||
}
|
||||
|
||||
card crowd_service{
|
||||
card backend {
|
||||
}
|
||||
|
||||
card bot_notifier {
|
||||
}
|
||||
|
||||
card frontend {
|
||||
card impl {
|
||||
entity CrowdReport{
|
||||
+ delete() -> None
|
||||
+ create() -> CrowdReport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
card node {
|
||||
}
|
||||
|
||||
card protocols{
|
||||
}
|
||||
|
||||
card report_builder{
|
||||
}
|
||||
|
||||
card utils {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
json options_dict {
|
||||
"cameras": ["cam1", "cam2"],
|
||||
"folders": ["folder1"],
|
||||
"zones": ["zone1"]
|
||||
}
|
||||
|
||||
CrowdReport ..|> CRA
|
||||
CRA::options -- options_dict
|
||||
|
||||
@enduml
|
||||
83
ivideon/puml/arch/AN_API_infra.puml
Normal file
83
ivideon/puml/arch/AN_API_infra.puml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
@startuml API Infrastructure with HAProxy
|
||||
skinparam linetype ortho
|
||||
|
||||
title Архитектура развертывания API (с HAProxy)
|
||||
|
||||
actor "Клиент" as Client
|
||||
cloud "Internet" as Internet
|
||||
|
||||
package "Kubernetes Cluster" {
|
||||
|
||||
package "Ingress Layer" {
|
||||
component "Nginx Ingress\nController" as NginxIngress #lightblue
|
||||
note right of NginxIngress
|
||||
- HTTPS (443) termination
|
||||
- TLS (Let's Encrypt)
|
||||
- X-Forwarded-For
|
||||
- Domains:
|
||||
* api.ivideon.com
|
||||
* api.stage-01.stg01-k8s.extcam.com
|
||||
end note
|
||||
}
|
||||
|
||||
package "Proxy Layer" {
|
||||
component "HAProxy\n(haproxy-central)" as HAProxy #lightgreen
|
||||
note right of HAProxy
|
||||
- Port 80 (HTTP)
|
||||
- ACL routing
|
||||
- Health checks (/status)
|
||||
- Backend: api4.service.ivideon:80
|
||||
end note
|
||||
}
|
||||
|
||||
package "Service Layer" {
|
||||
component "api4 Service" as Service #lightyellow
|
||||
note right of Service
|
||||
- Kubernetes Service
|
||||
- Port 80 → 8080
|
||||
- Load balancing
|
||||
- DNS: api4.service.ivideon
|
||||
end note
|
||||
}
|
||||
|
||||
package "Application Layer" {
|
||||
collections "api4 Pods" as Pods
|
||||
|
||||
component "Pod 1" as Pod1 {
|
||||
component "Tornado\nHTTP Server" as Tornado1 #orange
|
||||
note bottom of Tornado1
|
||||
- Port: 8080
|
||||
- xheaders: true
|
||||
- Workers: 4
|
||||
end note
|
||||
}
|
||||
|
||||
component "Pod 2-N" as PodN {
|
||||
component "Tornado\nHTTP Server" as TornadoN #orange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database "MongoDB\n(main)" as MongoDB
|
||||
database "MongoDB\n(user_registry)" as UserRegistry
|
||||
|
||||
Client --> Internet: HTTPS\nPOST /public/registration
|
||||
Internet --> NginxIngress: 443 (HTTPS)
|
||||
NginxIngress --> HAProxy: 80 (HTTP)\n+ X-Forwarded-For
|
||||
HAProxy --> Service: api4.service.ivideon:80\n(ACL: !has_api5_components)
|
||||
Service --> Pods: Round-robin LB
|
||||
Pods --> Pod1: 8080
|
||||
Pods --> PodN: 8080
|
||||
|
||||
Pod1 --> MongoDB: users.insert_one()
|
||||
Pod1 --> UserRegistry: check duplicate
|
||||
|
||||
note bottom of HAProxy
|
||||
**HAProxy ACL Routing:**
|
||||
- use_backend api4 if host_api !has_api5_components
|
||||
- Health check: GET /status
|
||||
- server-template api-four-srv 4
|
||||
- option redispatch
|
||||
end note
|
||||
|
||||
@enduml
|
||||
85
ivideon/puml/arch/AN_folders.puml
Normal file
85
ivideon/puml/arch/AN_folders.puml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
@startuml Folder Structure
|
||||
|
||||
package "MongoDB Collection: folders" {
|
||||
|
||||
object "Root Folder" as root {
|
||||
_id = "root_abc123"
|
||||
name = "__root__"
|
||||
parents = []
|
||||
objects = []
|
||||
owner_id = "123456"
|
||||
owner_name = "user@example.com"
|
||||
root = true
|
||||
}
|
||||
|
||||
object "Folder: Office" as office {
|
||||
_id = "folder_office_xyz"
|
||||
name = "Office"
|
||||
parents = ["root_abc123"]
|
||||
objects = [
|
||||
{object_type: "camera", object_id: "cam_1"},
|
||||
{object_type: "camera", object_id: "cam_2"}
|
||||
]
|
||||
owner_id = "123456"
|
||||
}
|
||||
|
||||
object "Folder: Warehouse" as warehouse {
|
||||
_id = "folder_warehouse_qwe"
|
||||
name = "Warehouse"
|
||||
parents = ["root_abc123"]
|
||||
objects = [
|
||||
{object_type: "camera", object_id: "cam_3"},
|
||||
{object_type: "server", object_id: "srv_1"}
|
||||
]
|
||||
owner_id = "123456"
|
||||
}
|
||||
|
||||
object "Subfolder: Entrance" as entrance {
|
||||
_id = "folder_entrance_asd"
|
||||
name = "Entrance"
|
||||
parents = ["root_abc123", "folder_office_xyz"]
|
||||
objects = [
|
||||
{object_type: "camera", object_id: "cam_4"}
|
||||
]
|
||||
owner_id = "123456"
|
||||
}
|
||||
}
|
||||
|
||||
object "User" as user {
|
||||
_id = 123456
|
||||
login = "user@example.com"
|
||||
root_folder = "root_abc123"
|
||||
}
|
||||
|
||||
user --> root : root_folder
|
||||
root --> office : subfolder
|
||||
root --> warehouse : subfolder
|
||||
office --> entrance : subfolder
|
||||
|
||||
note right of root
|
||||
При создании пользователя
|
||||
создается пустая root_folder
|
||||
|
||||
objects = []
|
||||
parents = []
|
||||
root = true
|
||||
end note
|
||||
|
||||
note right of office
|
||||
Пользователь может создавать
|
||||
папки для организации камер:
|
||||
- Офис
|
||||
- Склад
|
||||
- Парковка
|
||||
и т.д.
|
||||
end note
|
||||
|
||||
note right of entrance
|
||||
Поддерживается вложенность:
|
||||
parents = [root, office]
|
||||
|
||||
Уровень вложенности:
|
||||
level = len(parents) = 2
|
||||
end note
|
||||
|
||||
@enduml
|
||||
Loading…
Reference in a new issue