diff --git a/README.md b/README.md index 3239f2a..9c83328 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,170 @@ -# 🗂️ Trashbox - Минималистичный файловый сервер +# Trashbox — Минималистичный CGI-файловый сервер + +Легковесный файлообменник на чистом C для Linux/Debian. Работает через связку **Nginx (Reverse Proxy)** + **BusyBox `httpd` (CGI Backend)**. +> **Примечания:**
1) Nginx опционален, нужен только если необходимен Reverse Proxy, например, для SSL.
+> 2) Зачем? Хотелось что-то легковесного, но сам я не погромист, потому был применён вайбкод подход, делал вместе с DeepSeek. Вдохновленно [этим](https://github.com/metalx1000/Directory-Index-for-httpd) проектом, но там есть нюанс, сурсы закрыты, а что-то такое от рута пускать такое себе.
+> 3) [Здесь](https://home.mashup.su) можно потыкать и посмотреть как работает.
+> 4) Нейрокод вряд-ли качественный, да и конечно же без багов никуда, есть issue, PR, кому это инетерсно, конечно.
+> 5) Активно (не очень) какаю в репу в своем [GitLab](https://git.mashup.su/RomkaZVO/Trashbox), а тут скорее просто результат. + +## Скриншоты: +### Главная страница (светлая тема) + +https://home.mashup.su/trashpics/1.png +### Тёмная тема + +https://home.mashup.su/trashpics/2.png +### Мобильная версия + +https://home.mashup.su/trashpics/3.png +### Форма пароля + +https://home.mashup.su/trashpics/4.png +## Полный список возможностей + +### Авторизация и безопасность +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Пароль на папки** | Защита отдельных директорий через `.htpasswd`. | +| **2** | **POST-форма авторизации** | Безопасная передача пароля. | +| **3** | **Токены сессий** | Уникальные токены для доступа к защищённым разделам. | +| **4** | **Токен в localStorage** | Сохранение авторизации в браузере между переходами. | +| **5** | **Время жизни сессии** | Токен валиден **1 час**, после чего автоматически удаляется на сервере. | +| **6** | **Защита от брутфорса** | Лимит в **5 неудачных попыток** ввода пароля. | +| **7** | **Временный бан** | Блокировка доступа на **15 минут** при превышении лимита попыток. | + +### Навигация и работа с файлами +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Навигация** | Удобная (вроде) навигационная цепочка. | +| **2** | **Просмотр директорий** | Наглядное отображение полного содержимого папок. | +| **3** | **Гибкая сортировка** | Сортировка по имени, размеру и дате. | +| **4** | **Поиск файлов** | Рекурсивный поиск по имени внутри текущей директории. | +| **5** | **Скачивание файлов** | Прямая выдача файлов. | +| **6** | **Предпросмотр** | Модальное окно для просмотра картинок, видео, аудио, PDF и текста. | +| **7** | **Копирование ссылок** | Быстрое копирование чистой прямой ссылки на файл или папку. | +| **8** | **Иконки типов файлов** | Наглядные эмодзи-иконки для разграничения форматов. | +| **9** | **Подсчет размера папок** | Рекурсивный расчет и отображение общего объема папки. | +| **10** | **Сводная статистика** | Подсчет количества файлов, папок и их суммарного размера. | + +### Интерфейс и UX +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Адаптивный дизайн** | Полная оптимизация под ПК, планшеты и смартфоны. | +| **2** | **Тёмная / Светлая тема** | Переключение оформления с сохранением выбора. | +| **3** | **Сохранение контекста** | Навигация по хлебным крошкам без потери авторизации. | +| **4** | **Единая высота тулбара** | Одинаковый размер и стиль для элементов поиска и сортировки. | +| **5** | **Увеличенный UI на мобильных устройствах** | Крупные кнопки и элементы для удобного нажатия пальцами. | +| **6** | **Кнопка «Наверх»** | Быстрая прокрутка к началу страницы. | +| **7** | **Фиксированная сетка** | Фиксированная ширина иконок без «прыжков» верстки. | +| **8** | **Папки жирным шрифтом** | Визуальное отличие папок от файлов. | +| **9** | **Модальная авторизация** | Красивое окно запроса пароля при входе в закрытую папку. | + +### Мониторинг и логи +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Статус сервера** | Показывает память BusyBox/Nginx и нагрузку CPU. | +| **2** | **Асинхронные метрики** | Подгрузка статистики без блокировки основного UI и сессии. | + +### Технические особенности +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Чистый C** | Без PHP/Python/Node.js, максимум производительности при минимуме зависимостей. | +| **2** | **Универсальный CGI** | Работает через CGI на любом веб-сервере. | +| **3** | **Оптимизация под BusyBox** | Идеально подходит для встраиваемых систем и домашних серверов. | +| **4** | **~2 МБ RAM** | Минимальное потребление оперативной памяти. | +| **5** | **Хранение токенов в `/tmp`** | Автоматическая очистка сессий при перезагрузке системы. | +| **6** | **Конфигурация `config.ini`** | Гибкая настройка путей и параметров без пересборки. | + +### Оптимизация для мобильных устройств +| № | Особенность| Примечание | +| :-: | :--- | :--- | +| **1** | **Крупные кнопки** | Размер тач-зон 38–44px для удобства клика пальцем. | +| **2** | **Скрытие даты** | На маленьких экранах дата скрыта, отображается только размер. | +| **3** | **Увеличенные иконки** | Значки 32px вместо мелких 20px. | +--- + +## Установка зависимостей -### Установка зависимостей ``` bash sudo apt update -sudo apt install nginx busybox gcc +sudo apt install nginx busybox gcc make ``` -### Настройка структуры -```bash -mkdir -p /home/romkazvo/www/cgi-bin -cd /home/romkazvo/www/cgi-bin -git clone https://git.mashup.su/RomkaZVO/Trashbox.git +--- + +## Настройка структуры и загрузка + +``` bash +mkdir -p /patch/to/www/cgi-bin +cd /patch/to/www/cgi-bin +git clone https://git.mashup.su/RomkaZVO/Trashbox.git . ``` -### Структура +--- + +## Структура + ``` bash cgi-bin/ ├── Makefile ├── config.ini ├── template.html ├── style.css -├── .htpasswd +├── .htpasswd # Файл с паролями для защищенных папок ├── src/ -│ ├── main.c -│ ├── config.c / config.h -│ ├── auth.c / auth.h -│ ├── fs.c / fs.h -│ ├── render.c / render.h -│ ├── utils.c / utils.h -│ ├── style.c -│ └── status.c -├── index.cgi -├── style.cgi -└── status.cgi -403 и 404 опционально в папку уровнем выше, чем cgi-bin +│ ├── main.c # Точка входа главного CGI-скрипта +│ ├── config.c / .h # Парсинг конфигурации (config.ini) +│ ├── auth.c / .h # Авторизация, сессии и защита от брутфорса +│ ├── fs.c / .h # Навигация по файловой системе и операции +│ ├── render.c / .h # Шаблонизатор и генерация HTML +│ ├── utils.c / .h # Вспомогательные функции (URL encoding, escape, форматирование) +│ ├── style.c # Выдача и кэширование CSS +│ └── status.c # Мониторинг системных ресурсов +├── index.cgi # Бинарник списка файлов / навигации +├── style.cgi # Бинарник отдачи стилей +└── status.cgi # Бинарник системной статистики ``` -### Компиляция CGI -```bash +> **Примечание:** Кастомные страницы ошибок 403.html и 404.html опционально размещаются на уровень выше cgi-bin - в /patch/to/www/. + +--- + +## Компиляция и установка +``` bash cd src make sudo make install ``` -### Конфигурация Nginx -Файл конфигурации `/etc/nginx/sites-available/trashbox`:
-Образец здесь [тык](https://git.mashup.su/RomkaZVO/Trashbox/src/branch/main/busybox_nginx_reverse.txt) +--- -### Запуск BusyBox -```bash -busybox httpd -p 127.0.0.1:8050 -h /home/romkazvo/www +## Конфигурация Nginx + +Создайте файл конфигурации /etc/nginx/sites-available/service. +Образец конфигурации доступен по ссылке: +[Конфигурация BusyBox + Nginx](https://git.mashup.su/RomkaZVO/Trashbox/src/branch/main/busybox_nginx_reverse.txt) + +Активация виртуального хоста: +``` bash +sudo ln -s /etc/nginx/sites-available/service /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx ``` -### Папки с паролями -Создается папка, а для создания пароля нужно: -```bash -cat > /home/romkazvo/www/cgi-bin/.htpasswd << EOF -secret:mysecretpassword +--- + +## Запуск BusyBox + +Запуск фонового CGI-сервера: +``` bash +busybox httpd -p 127.0.0.1:8050 -h /patch/to/www +``` +--- + +## Настройка папок с паролями (.htpasswd) + +Папка создается обычным образом в файловой системе. Чтобы задать для нее пароль, добавьте запись в файл .htpasswd: +``` bash +cat >> /patch/to/www/cgi-bin/.htpasswd << EOF +secret_folder:mysecretpassword EOF ``` +--- + +### Лицензия +Ну это даже не смешно, это вайбкод проект, делайте что хотите, а если по нормальному, то GPLv3, но не обижусь если что-то и проприектарное будет. diff --git a/config.ini b/config.ini index 0fa3a68..b1086b2 100755 --- a/config.ini +++ b/config.ini @@ -18,3 +18,13 @@ text = txt,md,html,htm,css,js,json,xml,csv audio = mp3,wav,ogg,flac,m4a,aac video = mp4,webm,ogv,mov,avi,mkv pdf = pdf + +[security] +cookie_name = folder_auth +cookie_lifetime = 3600 +max_attempts = 5 +block_time = 900 + +[logging] +enable = 1 +log_file = /home/romkazvo/www/cgi-bin/logs/access.log diff --git a/imgs/1.png b/imgs/1.png new file mode 100755 index 0000000..47bd134 Binary files /dev/null and b/imgs/1.png differ diff --git a/imgs/2.png b/imgs/2.png new file mode 100755 index 0000000..3730293 Binary files /dev/null and b/imgs/2.png differ diff --git a/imgs/3.png b/imgs/3.png new file mode 100755 index 0000000..1083fc5 Binary files /dev/null and b/imgs/3.png differ diff --git a/imgs/4.png b/imgs/4.png new file mode 100755 index 0000000..4251b8f Binary files /dev/null and b/imgs/4.png differ diff --git a/src/auth.c b/src/auth.c index 1f8cedb..eba8a51 100644 --- a/src/auth.c +++ b/src/auth.c @@ -1,13 +1,29 @@ #include +#include #include +#include +#include +#include +#include #include "auth.h" #include "config.h" +#include "utils.h" + +#define TOKEN_DIR "/tmp/trashbox_tokens" +#define TOKEN_SALT "TrashBoxSecretSalt2024" + +// ============================================ +// ПРОВЕРКА ПАРОЛЯ +// ============================================ int check_folder_password(const char *folder_name, const char *password) { if (!password || !folder_name || password[0] == '\0') return 0; FILE *f = fopen(g_config.passwd_file, "r"); - if (!f) return 1; + if (!f) { + fprintf(stderr, "DEBUG: cannot open passwd file: %s\n", g_config.passwd_file); + return 0; + } char line[512]; while (fgets(line, sizeof(line), f)) { @@ -19,13 +35,14 @@ int check_folder_password(const char *folder_name, const char *password) { char *pass = colon + 1; const char *last_slash = strrchr(folder_name, '/'); const char *base_name = last_slash ? last_slash + 1 : folder_name; + if (strcmp(folder, folder_name) == 0 || strcmp(folder, base_name) == 0) { fclose(f); return strcmp(pass, password) == 0; } } fclose(f); - return 1; + return 0; } int is_folder_protected(const char *folder_path) { @@ -50,3 +67,195 @@ int is_folder_protected(const char *folder_path) { fclose(f); return 0; } + +// ============================================ +// ЛИМИТ ПОПЫТОК +// ============================================ + +void get_attempts_file(char *buf, size_t size, const char *folder_name, const char *ip) { + char safe_folder[256]; + strncpy(safe_folder, folder_name, sizeof(safe_folder) - 1); + safe_folder[sizeof(safe_folder) - 1] = '\0'; + for (char *p = safe_folder; *p; p++) { + if (*p == '/' || *p == '\\' || *p == '.') *p = '_'; + } + snprintf(buf, size, "/tmp/trashbox_attempts_%s_%s", safe_folder, ip); +} + +int check_attempts(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return 0; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + + FILE *f = fopen(attempts_file, "r"); + if (!f) return 0; + + int attempts; + time_t first_attempt_time; + + if (fscanf(f, "%d %ld", &attempts, &first_attempt_time) != 2) { + fclose(f); + return 0; + } + fclose(f); + + time_t now = time(NULL); + if (now - first_attempt_time > g_config.block_time) { + unlink(attempts_file); + return 0; + } + + return attempts >= g_config.max_attempts; +} + +void add_attempt(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + + int attempts = 0; + time_t first_attempt_time = time(NULL); + + FILE *f = fopen(attempts_file, "r"); + if (f) { + fscanf(f, "%d %ld", &attempts, &first_attempt_time); + fclose(f); + attempts++; + } else { + attempts = 1; + } + + f = fopen(attempts_file, "w"); + if (f) { + fprintf(f, "%d %ld\n", attempts, first_attempt_time); + fclose(f); + } +} + +void clear_attempts(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + unlink(attempts_file); +} + +// ============================================ +// ТОКЕНЫ +// ============================================ + +char* generate_token(const char *path) { + // ============================================ + // ФИНАЛЬНЫЙ ФИКС: очищаем путь от любых нежелательных символов + // ============================================ + char clean_path[1024]; + strncpy(clean_path, path, sizeof(clean_path) - 1); + clean_path[sizeof(clean_path) - 1] = '\0'; + + // Удаляем всё после '%' и другие нежелательные символы + char *p = clean_path; + while (*p) { + if (*p == '%' || *p == '\n' || *p == '\r') { + *p = '\0'; + break; + } + p++; + } + + // Если путь пустой — используем "default" + if (clean_path[0] == '\0') { + strcpy(clean_path, "default"); + } + + mkdir(TOKEN_DIR, 0700); + + time_t now = time(NULL); + char input[1024]; + snprintf(input, sizeof(input), "%ld_%s_%s", now, clean_path, TOKEN_SALT); + + unsigned long hash = 0; + for (int i = 0; input[i]; i++) { + hash = hash * 31 + input[i]; + } + + char token[256]; + snprintf(token, sizeof(token), "%lx_%ld", hash, now); + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + FILE *f = fopen(token_file, "w"); + if (!f) return NULL; + fprintf(f, "%s", clean_path); + fclose(f); + + return strdup(token); +} + +int check_token(const char *token, char *path, size_t path_size) { + if (!token || !token[0]) return 0; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + FILE *f = fopen(token_file, "r"); + if (!f) return 0; + + char stored_path[1024]; + if (fgets(stored_path, sizeof(stored_path), f) == NULL) { + fclose(f); + return 0; + } + fclose(f); + + stored_path[strcspn(stored_path, "\n")] = '\0'; + + // Обрезаем % в конце + char *pp = stored_path; + while (*pp) { + if (*pp == '%' || *pp == '\n' || *pp == '\r') { + *pp = '\0'; + break; + } + pp++; + } + + struct stat st; + if (stat(token_file, &st) != 0) return 0; + + time_t now = time(NULL); + if (now - st.st_mtime > 3600) { + unlink(token_file); + return 0; + } + + strncpy(path, stored_path, path_size - 1); + path[path_size - 1] = '\0'; + + return 1; +} + +void cleanup_old_tokens(void) { + DIR *dir = opendir(TOKEN_DIR); + if (!dir) return; + + struct dirent *entry; + time_t now = time(NULL); + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, entry->d_name); + + struct stat st; + if (stat(token_file, &st) == 0) { + if (now - st.st_mtime > 3600) { + unlink(token_file); + } + } + } + closedir(dir); +} \ No newline at end of file diff --git a/src/auth.h b/src/auth.h index f959f56..c5a1b91 100644 --- a/src/auth.h +++ b/src/auth.h @@ -3,5 +3,12 @@ int is_folder_protected(const char *folder_path); int check_folder_password(const char *folder_name, const char *password); +int check_attempts(const char *folder_name, const char *ip); +void add_attempt(const char *folder_name, const char *ip); +void clear_attempts(const char *folder_name, const char *ip); + +char* generate_token(const char *path); +int check_token(const char *token, char *path, size_t path_size); +void cleanup_old_tokens(void); #endif diff --git a/src/check_token.c b/src/check_token.c new file mode 100644 index 0000000..5522524 --- /dev/null +++ b/src/check_token.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include +#include + +#define TOKEN_DIR "/tmp/trashbox_tokens" + +int main() { + char *query_string = getenv("QUERY_STRING"); + if (!query_string) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + char *token_start = strstr(query_string, "token="); + if (!token_start) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len <= 0 || token_len >= 256) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + char token[256]; + strncpy(token, token_start, token_len); + token[token_len] = '\0'; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + struct stat st; + if (stat(token_file, &st) != 0) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + time_t now = time(NULL); + if (now - st.st_mtime > 3600) { + unlink(token_file); + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + printf("Content-type: text/plain\n\nvalid"); + return 0; +} diff --git a/src/config.c b/src/config.c index 7698fdc..97f6861 100644 --- a/src/config.c +++ b/src/config.c @@ -86,6 +86,18 @@ int load_config(const char *path) { else if (strcmp(key, "video") == 0) strncpy(g_config.preview_video, value, sizeof(g_config.preview_video) - 1); else if (strcmp(key, "pdf") == 0) strncpy(g_config.preview_pdf, value, sizeof(g_config.preview_pdf) - 1); } + else if (strcmp(current_section, "security") == 0) { + if (strcmp(key, "max_attempts") == 0) g_config.max_attempts = atoi(value); + else if (strcmp(key, "block_time") == 0) g_config.block_time = atoi(value); + } + else if (strcmp(current_section, "logging") == 0) { + if (strcmp(key, "enable") == 0) { + g_config.enable_access_log = atoi(value); + } else if (strcmp(key, "log_file") == 0) { + strncpy(g_config.log_file, value, sizeof(g_config.log_file) - 1); + g_config.log_file[sizeof(g_config.log_file) - 1] = '\0'; + } + } } fclose(f); @@ -104,6 +116,12 @@ void set_default_config(void) { strcpy(g_config.preview_audio, "mp3,wav,ogg,flac,m4a,aac"); strcpy(g_config.preview_video, "mp4,webm,ogv,mov,avi,mkv"); strcpy(g_config.preview_pdf, "pdf"); + + g_config.max_attempts = 5; + g_config.block_time = 900; + + strcpy(g_config.log_file, "/home/romkazvo/www/cgi-bin/logs/access.log"); + g_config.enable_access_log = 1; } int is_string_in_list(const char *str, const char *list) { diff --git a/src/config.h b/src/config.h index 8b1dacc..3a07b9a 100644 --- a/src/config.h +++ b/src/config.h @@ -15,6 +15,12 @@ typedef struct { char preview_audio[512]; char preview_video[512]; char preview_pdf[512]; + + int max_attempts; + int block_time; + + char log_file[1024]; + int enable_access_log; } config_t; extern config_t g_config; diff --git a/src/fs.c b/src/fs.c index 0921b70..0971555 100644 --- a/src/fs.c +++ b/src/fs.c @@ -124,8 +124,7 @@ void search_recursive(const char *base_path, const char *display_path, const cha if (stat(full_path, &file_stat) == 0) { if (my_strcasestr(entry->d_name, search_term) != NULL) { entry_t *result = &results[*count]; - strncpy(result->name, entry->d_name, sizeof(result->name) - 1); - result->name[sizeof(result->name) - 1] = '\0'; + snprintf(result->name, sizeof(result->name), "%s", entry->d_name); if (S_ISDIR(file_stat.st_mode)) { result->is_dir = 1; result->size = 0; @@ -135,7 +134,7 @@ void search_recursive(const char *base_path, const char *display_path, const cha strcpy(result->icon, "📁"); result->is_protected = 0; url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); - strncpy(result->file_url, display_subpath, sizeof(result->file_url) - 1); + snprintf(result->file_url, sizeof(result->file_url), "%s", display_subpath); (*count)++; } else if (S_ISREG(file_stat.st_mode)) { result->is_dir = 0; @@ -146,7 +145,7 @@ void search_recursive(const char *base_path, const char *display_path, const cha strcpy(result->icon, get_file_icon(entry->d_name)); result->is_protected = 0; url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); - strncpy(result->file_url, display_subpath, sizeof(result->file_url) - 1); + snprintf(result->file_url, sizeof(result->file_url), "%s", display_subpath); (*count)++; } } @@ -161,8 +160,10 @@ void search_recursive(const char *base_path, const char *display_path, const cha int compare_entries_sorted(const void *a, const void *b) { const entry_t *entryA = (const entry_t *)a; const entry_t *entryB = (const entry_t *)b; + if (entryA->is_dir && !entryB->is_dir) return -1; if (!entryA->is_dir && entryB->is_dir) return 1; + int result = 0; if (strcmp(g_params.sort_by, "size") == 0) { long long sizeA = entryA->is_dir ? entryA->dir_size : entryA->size; @@ -173,5 +174,6 @@ int compare_entries_sorted(const void *a, const void *b) { } else { result = strcasecmp(entryA->name, entryB->name); } + return result * g_params.sort_order; } diff --git a/src/fs.h b/src/fs.h index ee43d6f..1fb901c 100644 --- a/src/fs.h +++ b/src/fs.h @@ -9,7 +9,7 @@ typedef struct { long size; char icon[8]; char encoded_path[1024]; - char file_url[1024]; + char file_url[2048]; int is_protected; time_t mtime; int file_count; @@ -21,6 +21,7 @@ typedef struct { int sort_order; char search[256]; int recursive; + char view[16]; } query_params_t; extern query_params_t g_params; diff --git a/src/main.c b/src/main.c index fa916db..f90d953 100644 --- a/src/main.c +++ b/src/main.c @@ -21,13 +21,146 @@ int main() { g_params.sort_order = 1; g_params.search[0] = '\0'; g_params.recursive = 0; + strcpy(g_params.view, "list"); + char *method = getenv("REQUEST_METHOD"); char *query_string = getenv("QUERY_STRING"); + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; + char display_path[1024] = ""; char safe_display_path[1024] = ""; char base_path[1024]; strcpy(base_path, g_config.base_path); + // ============================================ + // ОБРАБОТКА POST (пароль через форму) + // ============================================ + if (method && strcmp(method, "POST") == 0) { + char *content_length_str = getenv("CONTENT_LENGTH"); + if (content_length_str) { + int content_length = atoi(content_length_str); + if (content_length > 0 && content_length < 65536) { + char *post_data = malloc(content_length + 1); + if (post_data) { + int read_len = fread(post_data, 1, content_length, stdin); + post_data[read_len] = '\0'; + + char post_path[1024] = ""; + char post_password[256] = ""; + + char *p = post_data; + while (*p) { + if (strncmp(p, "path=", 5) == 0) { + p += 5; + char *end = strchr(p, '&'); + int len = end ? (int)(end - p) : (int)strlen(p); + if (len > 0 && len < (int)sizeof(post_path) - 1) { + char encoded[1024]; + strncpy(encoded, p, len); + encoded[len] = '\0'; + url_decode_enhanced(encoded, post_path, sizeof(post_path)); + } + p += len; + if (*p == '&') p++; + } else if (strncmp(p, "password=", 9) == 0) { + p += 9; + char *end = strchr(p, '&'); + int len = end ? (int)(end - p) : (int)strlen(p); + if (len > 0 && len < (int)sizeof(post_password) - 1) { + char encoded[256]; + strncpy(encoded, p, len); + encoded[len] = '\0'; + url_decode_enhanced(encoded, post_password, sizeof(post_password)); + } + p += len; + if (*p == '&') p++; + } else { + p++; + } + } + + free(post_data); + + fprintf(stderr, "DEBUG POST: path='%s', password='%s'\n", post_path, post_password); + + if (post_path[0] && post_password[0]) { + if (check_attempts(post_path, remote_addr)) { + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=blocked\r\n\r\n", encoded_path); + return 0; + } + + const char *last_slash = strrchr(post_path, '/'); + const char *folder_name = last_slash ? last_slash + 1 : post_path; + + fprintf(stderr, "DEBUG: checking folder='%s'\n", folder_name); + + if (check_folder_password(folder_name, post_password)) { + fprintf(stderr, "DEBUG: password OK!\n"); + clear_attempts(post_path, remote_addr); + + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + + char clean_path[1024]; + strncpy(clean_path, post_path, sizeof(clean_path) - 1); + clean_path[sizeof(clean_path) - 1] = '\0'; + + char *pp = clean_path; + while (*pp) { + if (*pp == '%') { + *pp = '\0'; + break; + } + pp++; + } + + fprintf(stderr, "DEBUG: clean_path before token = '%s'\n", clean_path); + + char *token = generate_token(clean_path); + if (token) { + fprintf(stderr, "DEBUG: token generated: %s\n", token); + // ============================================ + // РЕДИРЕКТ С ТОКЕНОМ + // ============================================ + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&token=%s\r\n", encoded_path, token); + printf("\r\n"); + free(token); + return 0; + } else { + fprintf(stderr, "DEBUG: token generation failed\n"); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s\r\n", encoded_path); + printf("\r\n"); + return 0; + } + } else { + fprintf(stderr, "DEBUG: password FAILED!\n"); + add_attempt(post_path, remote_addr); + + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n", encoded_path); + printf("\r\n"); + return 0; + } + } else { + fprintf(stderr, "DEBUG: path or password empty!\n"); + } + } + } + } + } + + // ============================================ + // ОБРАБОТКА GET + // ============================================ if (query_string) { char *path_start = strstr(query_string, "path="); if (path_start) { @@ -61,6 +194,27 @@ int main() { } } + char *order_start = strstr(query_string, "order="); + if (order_start) { + order_start += 6; + if (*order_start == '1') { + g_params.sort_order = 1; + } else if (*order_start == '-') { + g_params.sort_order = -1; + } + } + + char *view_start = strstr(query_string, "view="); + if (view_start) { + view_start += 5; + char *view_end = strchr(view_start, '&'); + int view_len = view_end ? (int)(view_end - view_start) : (int)strlen(view_start); + if (view_len > 0 && view_len < (int)sizeof(g_params.view) - 1) { + strncpy(g_params.view, view_start, view_len); + g_params.view[view_len] = '\0'; + } + } + char *search_start = strstr(query_string, "search="); if (search_start) { search_start += 7; @@ -76,57 +230,16 @@ int main() { } } } - - // Обработка пароля (без кук) - if (strstr(query_string, "password=")) { - char *error = strstr(query_string, "error=1"); - if (!error) { - char folder_name[256] = ""; - if (display_path[0]) { - char *last_slash = strrchr(display_path, '/'); - if (last_slash) strcpy(folder_name, last_slash + 1); - else strcpy(folder_name, display_path); - if (is_folder_protected(folder_name)) { - char *pass_start = strstr(query_string, "password="); - if (pass_start) { - pass_start += 9; - char *pass_end = strchr(pass_start, '&'); - int pass_len = pass_end ? (int)(pass_end - pass_start) : (int)strlen(pass_start); - if (pass_len > 0 && pass_len < 256) { - char pass_buf[256]; - strncpy(pass_buf, pass_start, pass_len); - pass_buf[pass_len] = '\0'; - char decoded_pass[256]; - url_decode_enhanced(pass_buf, decoded_pass, sizeof(decoded_pass)); - if (decoded_pass[0] == '\0') { - char encoded_path[1024]; - url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Status: 302 Found\r\n"); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); - return 0; - } - if (!check_folder_password(folder_name, decoded_pass)) { - char encoded_path[1024]; - url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Status: 302 Found\r\n"); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); - return 0; - } - } else { - char encoded_path[1024]; - url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Status: 302 Found\r\n"); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); - return 0; - } - } - } - } - } - } + } + + static time_t last_cleanup = 0; + time_t now = time(NULL); + if (now - last_cleanup > 3600) { + cleanup_old_tokens(); + last_cleanup = now; } printf("Content-type: text/html; charset=utf-8\n\n"); print_template(base_path, display_path); return 0; -} +} \ No newline at end of file diff --git a/src/render.c b/src/render.c index a4eda9a..644576f 100644 --- a/src/render.c +++ b/src/render.c @@ -32,9 +32,35 @@ void buffer_append(buffer_t *buf, const char *str) { void buffer_free(buffer_t *buf) { free(buf->data); } +void get_token_part(char *buf, size_t size) { + buf[0] = '\0'; + char *query_string = getenv("QUERY_STRING"); + if (!query_string) return; + + char *token_start = strstr(query_string, "token="); + if (!token_start) return; + + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len > 0 && token_len < (int)size - 1) { + strncpy(buf, token_start, token_len); + buf[token_len] = '\0'; + } +} + void print_breadcrumb(buffer_t *buf, const char *display_path) { + char token_part[256] = ""; + get_token_part(token_part, sizeof(token_part)); + buffer_append(buf, "
\n"); - buffer_append(buf, " 🏠 Главная"); + buffer_append(buf, " 🏠 Главная"); + if (display_path && display_path[0]) { char temp_path[1024] = ""; char encoded[2048]; @@ -47,9 +73,17 @@ void print_breadcrumb(buffer_t *buf, const char *display_path) { url_encode(temp_path, encoded, sizeof(encoded)); char safe_token[512]; html_escape(token, safe_token, sizeof(safe_token)); - char link[4096]; - snprintf(link, sizeof(link), "%s", encoded, safe_token); - buffer_append(buf, link); + + buffer_append(buf, ""); + buffer_append(buf, safe_token); + buffer_append(buf, ""); + token = strtok(NULL, "/"); } free(path_copy); @@ -58,6 +92,24 @@ void print_breadcrumb(buffer_t *buf, const char *display_path) { } void print_password_form(buffer_t *buf, const char *folder_name) { + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; + + if (check_attempts(folder_name, remote_addr)) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
🚫
\n"); + buffer_append(buf, "

Доступ заблокирован

\n"); + buffer_append(buf, "

Слишком много неудачных попыток. Подождите 15 минут.

\n"); + buffer_append(buf, " ← Вернуться на главную\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + return; + } + + char encoded_path[1024]; + url_encode(folder_name, encoded_path, sizeof(encoded_path)); + buffer_append(buf, "
\n"); buffer_append(buf, "
\n"); buffer_append(buf, "
🔒
\n"); @@ -67,16 +119,15 @@ void print_password_form(buffer_t *buf, const char *folder_name) { html_escape(folder_name, safe_name, sizeof(safe_name)); buffer_append(buf, safe_name); buffer_append(buf, "

\n"); - buffer_append(buf, "
\n"); + buffer_append(buf, " \n"); buffer_append(buf, " \n"); - buffer_append(buf, " \n"); + buffer_append(buf, " \n"); buffer_append(buf, " \n"); buffer_append(buf, "
\n"); buffer_append(buf, "

"); + char *query_string = getenv("QUERY_STRING"); if (query_string && strstr(query_string, "error=1")) { buffer_append(buf, "❌ Неверный пароль! Попробуйте снова."); @@ -88,60 +139,83 @@ void print_password_form(buffer_t *buf, const char *folder_name) { } void print_content(buffer_t *buf, const char *base_path, const char *display_path) { + fprintf(stderr, "DEBUG: print_content START, display_path='%s'\n", display_path ? display_path : "(null)"); + + // ============================================ + // ОБЪЯВЛЯЕМ entries В НАЧАЛЕ ФУНКЦИИ + // ============================================ DIR *dir; struct dirent *entry; struct stat file_stat; char full_path[1024]; - entry_t *entries = malloc(sizeof(entry_t) * g_config.max_entries); - if (!entries) { - buffer_append(buf, "

Ошибка выделения памяти
"); - return; - } + entry_t *entries = NULL; int entry_count = 0, dir_count = 0, file_count = 0; long long total_size = 0; int is_recursive_search = 0; + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; - // === ПАРСИМ ПАРОЛЬ ДО ВСЕГО ОСТАЛЬНОГО === - char folder_name[256] = ""; - char *password = NULL; char *query_string = getenv("QUERY_STRING"); + int is_root = 0; + if (!query_string || !strstr(query_string, "path=")) { + fprintf(stderr, "DEBUG: no path=, rendering root\n"); + is_root = 1; + } - if (query_string) { - char *pass_start = strstr(query_string, "password="); - if (pass_start) { - pass_start += 9; - char *pass_end = strchr(pass_start, '&'); - int pass_len = pass_end ? (int)(pass_end - pass_start) : (int)strlen(pass_start); - if (pass_len > 0 && pass_len < 256) { - char pass_buf[256]; - strncpy(pass_buf, pass_start, pass_len); - pass_buf[pass_len] = '\0'; - char decoded_pass[256]; - url_decode_enhanced(pass_buf, decoded_pass, sizeof(decoded_pass)); - password = decoded_pass; + char token_part[256] = ""; + get_token_part(token_part, sizeof(token_part)); + fprintf(stderr, "DEBUG: token_part='%s'\n", token_part); + + int has_valid_token = 0; + + if (!is_root && query_string) { + char *token_start = strstr(query_string, "token="); + if (token_start) { + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len > 0 && token_len < 256) { + char token[256]; + strncpy(token, token_start, token_len); + token[token_len] = '\0'; + + char token_path[1024]; + if (check_token(token, token_path, sizeof(token_path))) { + if (strcmp(token_path, display_path) == 0) { + has_valid_token = 1; + fprintf(stderr, "DEBUG: valid token for '%s'\n", display_path); + } + } } } } - if (display_path && display_path[0]) { + if (!is_root && !has_valid_token && display_path && display_path[0]) { char *last_slash = strrchr(display_path, '/'); - if (last_slash) strcpy(folder_name, last_slash + 1); - else strcpy(folder_name, display_path); - } - - // === ОТЛАДКА === - fprintf(stderr, "DEBUG: folder_name = '%s'\n", folder_name); - fprintf(stderr, "DEBUG: password = '%s'\n", password ? password : "NULL"); - if (password) { - fprintf(stderr, "DEBUG: check_folder_password = %d\n", check_folder_password(folder_name, password)); - } - - if (is_folder_protected(folder_name)) { - if (!password || !check_folder_password(folder_name, password)) { + char *folder_name = last_slash ? last_slash + 1 : (char*)display_path; + + if (query_string && strstr(query_string, "error=1")) { + fprintf(stderr, "DEBUG: error=1, showing password form\n"); print_password_form(buf, folder_name); - free(entries); return; } + + if (is_folder_protected(folder_name)) { + fprintf(stderr, "DEBUG: no valid token, showing password form\n"); + // Добавляем скрипт очистки localStorage + buffer_append(buf, "\n"); + print_password_form(buf, folder_name); + return; + } + } + + // ============================================ + // ВЫДЕЛЯЕМ ПАМЯТЬ ДЛЯ entries + // ============================================ + entries = malloc(sizeof(entry_t) * g_config.max_entries); + if (!entries) { + buffer_append(buf, "
Ошибка выделения памяти
"); + return; } if (g_params.search[0] != '\0') { @@ -184,15 +258,15 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat dir = opendir(alt_path); if (!dir) { buffer_append(buf, "
\n"); - buffer_append(buf, "
📁
\n"); - char *display_base = strstr(base_path, "/www/"); - if (!display_base) display_base = (char*)base_path; - buffer_append(buf, "

Ошибка открытия: "); - buffer_append(buf, display_base); - buffer_append(buf, "

\n"); - buffer_append(buf, "

Папка не найдена

\n"); - buffer_append(buf, "

← Вернуться на главную

\n"); - buffer_append(buf, "
\n"); + buffer_append(buf, "
📁
\n"); + char *display_base = strstr(base_path, "/www/"); + if (!display_base) display_base = (char*)base_path; + buffer_append(buf, "

Ошибка открытия директории: "); + buffer_append(buf, display_base); + buffer_append(buf, "

\n"); + buffer_append(buf, "

Папка не найдена

\n"); + buffer_append(buf, "

← Вернуться на главную

\n"); + buffer_append(buf, "
\n"); free(entries); return; } @@ -210,8 +284,7 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat if (hidden) continue; snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); if (stat(full_path, &file_stat) == 0) { - strncpy(entries[entry_count].name, entry->d_name, sizeof(entries[0].name) - 1); - entries[entry_count].name[sizeof(entries[0].name) - 1] = '\0'; + snprintf(entries[entry_count].name, sizeof(entries[0].name), "%s", entry->d_name); if (S_ISDIR(file_stat.st_mode)) { entries[entry_count].is_dir = 1; entries[entry_count].size = 0; @@ -240,7 +313,9 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat entries[entry_count].is_protected = 0; char encoded_filename[1024]; url_encode(entry->d_name, encoded_filename, sizeof(encoded_filename)); - snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s%s%s", display_path, display_path[0] ? "/" : "", encoded_filename); + char temp_url[2048]; + snprintf(temp_url, sizeof(temp_url), "%s%s%s", display_path, display_path[0] ? "/" : "", encoded_filename); + snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s", temp_url); total_size += file_stat.st_size; file_count++; } else { @@ -260,42 +335,92 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat return; } } + qsort(entries, entry_count, sizeof(entry_t), compare_entries_sorted); + char encoded_path[1024]; url_encode(display_path, encoded_path, sizeof(encoded_path)); char safe_search[512]; html_escape(g_params.search, safe_search, sizeof(safe_search)); + + int next_order = g_params.sort_order * -1; + char order_str[4]; + snprintf(order_str, sizeof(order_str), "%d", next_order); + + char active_name[64] = ""; + char active_size[64] = ""; + char active_date[64] = ""; + if (strcmp(g_params.sort_by, "name") == 0) { strcpy(active_name, " active"); } + else if (strcmp(g_params.sort_by, "size") == 0) { strcpy(active_size, " active"); } + else if (strcmp(g_params.sort_by, "date") == 0) { strcpy(active_date, " active"); } + + char arrow[8] = ""; + if (g_params.sort_order == 1) strcpy(arrow, "🔽"); + else strcpy(arrow, "🔼"); + + char link_with_token[512]; + if (token_part[0]) { + snprintf(link_with_token, sizeof(link_with_token), "&%s", token_part); + } else { + link_with_token[0] = '\0'; + } + buffer_append(buf, "
\n"); buffer_append(buf, "
\n"); buffer_append(buf, "
\n"); buffer_append(buf, " \n"); - buffer_append(buf, " \n"); + } + buffer_append(buf, " \n"); buffer_append(buf, " \n"); buffer_append(buf, "
\n"); buffer_append(buf, "
\n"); - buffer_append(buf, "
\n"); - buffer_append(buf, " Сортировка:\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " Сорт.\n"); + buffer_append(buf, " 📝 Имя\n"); + buffer_append(buf, "&sort=name&order="); + buffer_append(buf, order_str); + buffer_append(buf, link_with_token); + buffer_append(buf, "\" class=\"sort-btn"); + buffer_append(buf, active_name); + buffer_append(buf, "\">📝 Имя "); + if (strcmp(g_params.sort_by, "name") == 0) buffer_append(buf, arrow); + buffer_append(buf, "\n"); + buffer_append(buf, " 📊 Размер\n"); + buffer_append(buf, "&sort=size&order="); + buffer_append(buf, order_str); + buffer_append(buf, link_with_token); + buffer_append(buf, "\" class=\"sort-btn"); + buffer_append(buf, active_size); + buffer_append(buf, "\">📊 Размер "); + if (strcmp(g_params.sort_by, "size") == 0) buffer_append(buf, arrow); + buffer_append(buf, "\n"); + buffer_append(buf, " 📅 Дата\n"); + buffer_append(buf, "&sort=date&order="); + buffer_append(buf, order_str); + buffer_append(buf, link_with_token); + buffer_append(buf, "\" class=\"sort-btn"); + buffer_append(buf, active_date); + buffer_append(buf, "\">📅 Дата "); + if (strcmp(g_params.sort_by, "date") == 0) buffer_append(buf, arrow); + buffer_append(buf, "\n"); + buffer_append(buf, "
\n"); buffer_append(buf, "
\n"); + if (is_recursive_search) { buffer_append(buf, "
"); buffer_append(buf, "🔍 Результаты поиска по всему сайту: "); @@ -304,6 +429,7 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat buffer_append(buf, tmp); buffer_append(buf, " файлов/папок найдено
\n"); } + buffer_append(buf, "
\n"); } else { buffer_append(buf, "
\n"); - char dir_link[512]; - snprintf(dir_link, sizeof(dir_link), - " \n", - entries[i].encoded_path); - buffer_append(buf, dir_link); + buffer_append(buf, " \n"); buffer_append(buf, " "); buffer_append(buf, entries[i].icon); buffer_append(buf, "\n"); @@ -377,6 +517,10 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat buffer_append(buf, "
\n"); buffer_append(buf, " \n"); if (entries[i].dir_size > 0) { char size_str[32]; @@ -473,14 +617,18 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat buffer_append(buf, stats); } free(entries); + fprintf(stderr, "DEBUG: print_content END\n"); } void print_template(const char *base_path, const char *display_path) { FILE *file = fopen(g_config.template_path, "r"); if (!file) { + fprintf(stderr, "DEBUG: cannot open template: %s\n", g_config.template_path); printf("Error: Cannot read template\n"); return; } + fprintf(stderr, "DEBUG: template opened successfully\n"); + buffer_t output; buffer_init(&output); char line[4096]; @@ -494,6 +642,16 @@ void print_template(const char *base_path, const char *display_path) { } } fclose(file); + + fprintf(stderr, "DEBUG: output size = %zu bytes\n", output.size); + if (output.size == 0) { + fprintf(stderr, "DEBUG: WARNING! output is empty!\n"); + buffer_free(&output); + return; + } fwrite(output.data, 1, output.size, stdout); + fflush(stdout); + fprintf(stderr, "DEBUG: fwrite done, flushed\n"); + buffer_free(&output); } diff --git a/src/render.h b/src/render.h index 12a0752..0b38b6b 100644 --- a/src/render.h +++ b/src/render.h @@ -13,5 +13,6 @@ void buffer_init(buffer_t *buf); void buffer_append(buffer_t *buf, const char *str); void buffer_free(buffer_t *buf); void print_template(const char *base_path, const char *display_path); +void render_folder(buffer_t *buf, const char *base_path, const char *display_path); #endif diff --git a/src/status.c b/src/status.c index a674bdc..42a82ba 100644 --- a/src/status.c +++ b/src/status.c @@ -1,3 +1,4 @@ +// /home/romkazvo/www/cgi-bin/src/status.c #include #include #include @@ -35,18 +36,62 @@ int get_process_memory(const char *process_name, long *memory_kb) { return result == 1; } +float get_cpu_load() { + FILE *fp = fopen("/proc/loadavg", "r"); + if (!fp) return 0.0f; + + float load; + fscanf(fp, "%f", &load); + fclose(fp); + + return load; +} + +void get_memory_usage(long *used_mb, long *total_mb) { + FILE *fp = fopen("/proc/meminfo", "r"); + if (!fp) { + *used_mb = 0; + *total_mb = 0; + return; + } + + char line[128]; + long total = 0, available = 0; + + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "MemTotal: %ld kB", &total) == 1) { + // сохраняем + } else if (sscanf(line, "MemAvailable: %ld kB", &available) == 1) { + break; + } + } + fclose(fp); + + *total_mb = total / 1024; + *used_mb = (total - available) / 1024; +} + int main() { printf("Content-type: text/plain; charset=utf-8\n\n"); long busybox_kb = 0, nginx_kb = 0; - get_process_memory("busybox", &busybox_kb); nginx_kb = get_nginx_memory(); - printf("BusyBox: %ld KB | Nginx: %ld MB | Всего: %.1f MB", - busybox_kb, - nginx_kb / 1024, - (busybox_kb + nginx_kb) / 1024.0); + double busybox_mb = busybox_kb / 1024.0; + double nginx_mb = nginx_kb / 1024.0; + double total_mb = busybox_mb + nginx_mb; + + float cpu_load = get_cpu_load(); + + long used_mb = 0, total_sys_mb = 0; + get_memory_usage(&used_mb, &total_sys_mb); + + // Две строки + printf("BusyBox: %.1f MB | Nginx: %.1f MB | Всего: %.1f MB\n", + busybox_mb, nginx_mb, total_mb); + printf("CPU Load: %.2f | Память: %ld / %ld MB (%.0f%%)", + cpu_load, used_mb, total_sys_mb, total_sys_mb > 0 ? (float)used_mb / total_sys_mb * 100 : 0); return 0; } diff --git a/src/utils.c b/src/utils.c index d1ee12d..b7caffe 100644 --- a/src/utils.c +++ b/src/utils.c @@ -80,6 +80,9 @@ void url_decode_enhanced(const char *src, char *dst, size_t dst_size) { } src += 3; } else { + // ============================================ + // ФИКС: если % не валидный — просто пропускаем его + // ============================================ src++; } } else if (*src == '+') { @@ -92,4 +95,4 @@ void url_decode_enhanced(const char *src, char *dst, size_t dst_size) { } } *p = '\0'; -} +} \ No newline at end of file diff --git a/style.css b/style.css index 7fdade0..98a550f 100755 --- a/style.css +++ b/style.css @@ -1,488 +1,519 @@ +/* ============================================ + БАЗА + ============================================ */ :root { --bg-primary: #f8f9fa; --bg-secondary: #ffffff; - --bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - --text-primary: #333333; - --text-secondary: #5f6368; - --text-muted: #6c757d; - --border-color: #e9ecef; - --border-light: #f1f3f4; - --accent-blue: #1a73e8; - --accent-green: #188038; - --hover-bg: #f8f9fa; - --shadow: 0 2px 4px rgba(0,0,0,0.1); - /* === ПЕРЕМЕННЫЕ ДЛЯ МОДАЛКИ === */ - --modal-bg: #ffffff; - --modal-text: #333333; - --modal-header-bg: #f8f9fa; - --modal-border: #e9ecef; + --bg-header: linear-gradient(135deg, #2d3436, #636e72); + --text-primary: #2d3436; + --text-secondary: #636e72; + --text-muted: #b2bec3; + --border-color: #dfe6e9; + --border-light: #f1f2f6; + --accent-blue: #0984e3; + --accent-orange: #e17055; + --accent-green: #00b894; + --hover-bg: #f1f2f6; + --shadow: 0 4px 12px rgba(0,0,0,0.04); + --radius: 12px; } .dark-theme { - --bg-primary: #121212; - --bg-secondary: #1e1e1e; - --bg-header: linear-gradient(135deg, #4a5568 0%, #2d3748 100%); + --bg-primary: #1e1e1e; + --bg-secondary: #2d2d2d; + --bg-header: linear-gradient(135deg, #1e1e1e, #2d2d2d); --text-primary: #e0e0e0; - --text-secondary: #a0a0a0; - --text-muted: #888888; - --border-color: #2d3748; - --border-light: #2d3748; - --accent-blue: #63b3ed; - --accent-green: #68d391; - --hover-bg: #2d3748; - --shadow: 0 2px 4px rgba(0,0,0,0.3); - /* === ПЕРЕМЕННЫЕ ДЛЯ МОДАЛКИ В ТЁМНОЙ ТЕМЕ === */ - --modal-bg: #1e1e1e; - --modal-text: #e0e0e0; - --modal-header-bg: #2d3748; - --modal-border: #2d3748; + --text-secondary: #b0b0b0; + --text-muted: #6c6c6c; + --border-color: #3d3d3d; + --border-light: #333333; + --accent-blue: #74b9ff; + --accent-orange: #fdcb6e; + --accent-green: #55efc4; + --hover-bg: #3d3d3d; + --shadow: 0 4px 12px rgba(0,0,0,0.2); + --radius: 12px; } -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); +/* ============================================ + ТЕЛО + ============================================ */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); line-height: 1.6; min-height: 100vh; - transition: none; + transition: background 0.25s ease, color 0.25s ease; } body.loaded * { - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; + transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } -.container { - max-width: 100%; - margin: 0 auto; - background: var(--bg-secondary); +/* ============================================ + КОНТЕЙНЕР + ============================================ */ +.container { + max-width: 100%; + margin: 0 auto; + background: var(--bg-secondary); min-width: 320px; min-height: 100vh; box-shadow: var(--shadow); + transition: background 0.25s ease, box-shadow 0.25s ease; } @media (min-width: 768px) { - .container { - max-width: 1000px; - margin: 10px auto; + .container { + max-width: 1100px; + margin: 16px auto; min-height: auto; - border-radius: 8px; + border-radius: var(--radius); overflow: hidden; + box-shadow: var(--shadow); } } -.theme-toggle { - position: fixed; - top: 10px; - right: 10px; - background: rgba(255,255,255,0.2); - border: none; - border-radius: 50%; - width: 50px; - height: 50px; - font-size: 1.3rem; - cursor: pointer; - z-index: 1000; - backdrop-filter: blur(10px); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; -} - -.dark-theme .theme-toggle { - background: rgba(0,0,0,0.2); - color: var(--text-primary); -} - -.scroll-top { - position: fixed; - bottom: 20px; - right: 20px; - width: 50px; - height: 50px; - background: var(--accent-blue); +/* ============================================ + ШАПКА + ============================================ */ +.header { + background: var(--bg-header); color: white; - border: none; - border-radius: 50%; - font-size: 1.3rem; - cursor: pointer; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; - z-index: 1000; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - display: flex; - align-items: center; - justify-content: center; -} - -.scroll-top.visible { - opacity: 1; - visibility: visible; -} - -.scroll-top:hover { - background: var(--accent-green); - transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(0,0,0,0.3); -} - -.theme-toggle:hover { - transform: scale(1.1); - background: rgba(255,255,255,0.3); -} - -.dark-theme .theme-toggle:hover { - background: rgba(0,0,0,0.3); -} - -.header { - background: var(--bg-header); - color: white; - padding: 1.5rem 1rem; - text-align: center; + padding: 2rem 1.5rem; + text-align: center; position: relative; } -h1 { - font-size: 1.8rem; - font-weight: 700; - margin-bottom: 0.5rem; +.header h1 { + font-size: 2rem; + font-weight: 700; + margin: 0; + letter-spacing: -0.5px; } -.content { - padding: 1rem; - min-width: 0; +.header .subtitle { + opacity: 0.85; + font-size: 1.05rem; + margin-top: 0.25rem; + font-weight: 400; } -.breadcrumb { - background: var(--bg-primary); - padding: 1rem; - margin: 0 0 1rem 0; - border-bottom: 1px solid var(--border-color); - font-size: 0.9rem; - overflow-x: auto; +/* ============================================ + ОПИСАНИЕ (НЕ УВЕЛИЧИВАЕМ) + ============================================ */ +.site-description { + text-align: center; + padding: 0.75rem 1.5rem; + font-size: 1rem; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); + font-weight: 400; + letter-spacing: 0.2px; +} + +/* ============================================ + ХЛЕБНЫЕ КРОШКИ + ============================================ */ +.breadcrumb { + background: var(--bg-primary); + padding: 0.6rem 1.25rem; + border-bottom: 1px solid var(--border-color); + font-size: 0.85rem; + overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; -} - -.breadcrumb a { - color: var(--accent-blue); - text-decoration: none; - font-weight: 500; - display: inline-block; -} - -.breadcrumb a:hover { - text-decoration: underline; -} - -ul { - list-style: none; - padding: 0; - margin: 0; -} - -li { - padding: 1rem 0.5rem; - border-bottom: 1px solid var(--border-light); - display: flex; + color: var(--text-secondary); + display: flex; align-items: center; + gap: 0.3rem; flex-wrap: nowrap; } -li:last-child { border-bottom: none; } - -li:hover { - background-color: var(--hover-bg); +.breadcrumb a { + color: var(--accent-blue); + text-decoration: none; + font-weight: 500; + transition: color 0.15s ease; + padding: 0.1rem 0.2rem; } -/* Стили для строки файла */ +.breadcrumb a:hover { + color: var(--accent-orange); + text-decoration: underline; +} + +.breadcrumb .separator { + color: var(--text-muted); + user-select: none; + font-weight: 300; + margin: 0 0.1rem; +} + +/* ============================================ + ТУЛБАР + ============================================ */ +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 1rem; + padding: 0.6rem 1.25rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-light); + transition: background 0.2s ease; +} + +/* Поиск */ +.search-box { + flex: 1 1 220px; + min-width: 150px; +} + +.search-box form { + display: flex; + gap: 0.4rem; + align-items: center; +} + +.search-box input[type="text"] { + flex: 1; + padding: 0.5rem 1rem; + border: 1.5px solid var(--border-color); + border-radius: 50px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.85rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; + min-width: 80px; + height: 38px; + box-sizing: border-box; +} + +.search-box input[type="text"]:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(9, 132, 227, 0.12); +} + +.search-box button { + padding: 0 1.2rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 50px; + cursor: pointer; + font-weight: 600; + font-size: 0.85rem; + transition: background 0.2s ease, transform 0.15s ease; + white-space: nowrap; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.search-box button:hover { + background: var(--accent-orange); + transform: scale(1.02); +} + +/* Группы настроек */ +.settings-group { + display: flex; + align-items: center; + gap: 0.25rem; + flex-wrap: wrap; +} + +.settings-label { + color: var(--text-muted); + font-weight: 500; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.3px; + margin-right: 0.15rem; +} + +/* Разделитель между группами */ +.settings-divider { + width: 1px; + height: 30px; + background: var(--border-color); + margin: 0 0.25rem; +} + +@media (max-width: 768px) { + .settings-divider { + display: none; + } +} + +/* Кнопки вида и сортировки */ +.view-btn, +.sort-btn { + padding: 0 0.9rem; + background: transparent; + border: 1.5px solid var(--border-color); + border-radius: 50px; + color: var(--text-secondary); + font-size: 0.78rem; + cursor: pointer; + transition: all 0.15s ease; + font-weight: 500; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + height: 38px; + line-height: 1; + gap: 0.2rem; + box-sizing: border-box; + white-space: nowrap; +} + +.view-btn:hover, +.sort-btn:hover { + background: var(--hover-bg); + border-color: var(--accent-blue); + color: var(--text-primary); +} + +.view-btn.active, +.sort-btn.active { + background: var(--accent-blue); + color: white !important; + border-color: var(--accent-blue); +} + +.view-btn .icon, +.sort-btn .icon { + font-size: 0.85rem; + line-height: 1; +} + +/* Скрываем старый settings-row */ +.settings-row { + display: none !important; +} + +/* ============================================ + СПИСОК ФАЙЛОВ + ============================================ */ +ul { + list-style: none; + padding: 0; + margin: 0; +} + +li { + padding: 0.6rem 1.25rem; + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + flex-wrap: nowrap; + transition: background 0.15s ease; +} + +li:hover { + background: var(--hover-bg); +} + +li:last-child { + border-bottom: none; +} + +li.section-title { + color: var(--text-secondary); + font-size: 0.8rem; + font-weight: 600; + padding: 0.5rem 1.25rem; + border-bottom: 2px solid var(--border-color); + background: var(--bg-primary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 0.5rem; +} + +li.section-title:first-of-type { + margin-top: 0; +} + +/* ============================================ + ФАЙЛОВАЯ СТРОКА — ФИНАЛЬНЫЙ ФИКС + ============================================ */ .file-row { display: flex; align-items: center; justify-content: space-between; flex-grow: 1; min-width: 0; - gap: 1rem; + gap: 0.75rem; flex-wrap: nowrap; + width: 100%; } -.file-link { - color: var(--accent-blue); - font-weight: 500; - text-decoration: none; - display: flex; - align-items: center; - flex-grow: 1; - min-width: 0; -} - +.file-link, .dir-link { - color: var(--accent-green); - font-weight: 600; - text-decoration: none; display: flex; align-items: center; - flex-grow: 1; + flex: 1 1 auto; + min-width: 0; + gap: 0.6rem; + overflow: hidden; + text-decoration: none; + color: var(--text-primary); +} + +.file-link .file-name { + font-weight: 500; +} + +.dir-link .file-name { + font-weight: 600; +} + +.file-link:hover .file-name { + color: var(--accent-blue); +} + +.dir-link:hover .file-name { + color: var(--accent-orange); +} + +.file-icon { + font-size: 1.3rem; + flex-shrink: 0; + width: 28px; + min-width: 28px; + max-width: 28px; + text-align: center; + line-height: 1; +} + +.file-name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.92rem; + transition: color 0.15s ease; } .file-controls { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.3rem; flex-shrink: 0; flex-wrap: nowrap; + margin-left: auto; } .file-meta { color: var(--text-muted); - font-size: 0.85rem; - font-weight: 600; + font-size: 0.75rem; + font-weight: 500; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; + flex-shrink: 0; } .file-meta.date { - min-width: 90px; + min-width: 80px; + width: 80px; } .file-meta.size { - min-width: 70px; + min-width: 60px; + width: 60px; + text-align: right; + flex-shrink: 0; } -@media (max-width: 768px) { - .file-meta { - font-size: 0.7rem; - } - .file-meta.date { - min-width: 75px; - } - .file-meta.size { - min-width: 60px; - } -} - -@media (max-width: 480px) { - .file-meta.date { - display: none !important; - } - .file-meta.size { - font-size: 0.65rem; - min-width: 50px; - } -} - -.file-icon { - margin-right: 0.75rem; - font-size: 1.3rem; - flex-shrink: 0; - width: 24px; - text-align: center; -} - -.file-name { - flex-grow: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.95rem; -} - -.section-title { - color: var(--text-secondary); - font-size: 0.9rem; - font-weight: 600; - margin: 1.5rem 0 0.5rem 0; - padding: 0.75rem 0.5rem; - border-bottom: 2px solid var(--border-light); - text-transform: uppercase; - letter-spacing: 0.5px; - background: var(--bg-primary); -} - -.stats { - text-align: center; - color: var(--text-secondary); - margin-top: 2rem; - padding-top: 1.5rem; - border-top: 1px solid var(--border-color); - font-size: 0.9rem; -} - -.empty-state { - text-align: center; - padding: 3rem 1rem; - color: var(--text-secondary); -} - -.empty-state .icon { - font-size: 3rem; - margin-bottom: 1rem; - opacity: 0.5; -} - -.footer { - background: var(--bg-primary); - border-top: 1px solid var(--border-color); - padding: 1rem; - text-align: center; - color: var(--text-secondary); - font-size: 0.9rem; -} - -.footer-content { - max-width: 1000px; - margin: 0 auto; - text-align: center; -} - -.footer-content span, -.footer-content a { - display: inline; - vertical-align: baseline; -} - -.footer-link { - color: var(--accent-blue); - text-decoration: none; - font-weight: 500; - transition: opacity 0.2s ease; - margin-left: 0.3rem; -} - -.footer-link:hover { - opacity: 0.8; - text-decoration: underline; -} - -/* ================================================ - ЕДИНЫЙ СТИЛЬ ДЛЯ ВСЕХ КНОПОК - ================================================ */ -.preview-btn, .copy-btn, .download-btn { +/* Кнопки — фиксированный размер */ +.preview-btn, +.copy-btn, +.download-btn { background: transparent; - color: var(--text-secondary); border: 1px solid var(--border-color); border-radius: 6px; width: 32px; height: 32px; + min-width: 32px; + min-height: 32px; + max-width: 32px; + max-height: 32px; cursor: pointer; - font-size: 0.85rem; + font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; - transition: all 0.2s ease; - opacity: 0.7; + transition: all 0.15s ease; + opacity: 0.6; + color: var(--text-secondary); + padding: 0; + box-sizing: border-box; flex-shrink: 0; } -.preview-btn:hover, .copy-btn:hover, .download-btn:hover { - background: var(--accent-blue); - color: white; - border-color: var(--accent-blue); +.preview-btn:hover, +.copy-btn:hover, +.download-btn:hover { opacity: 1; transform: scale(1.05); } -/* ================================================ - ТУЛБАР (поиск + сортировка) - ================================================ */ -.toolbar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 1rem; - padding: 0.75rem 0.5rem; - margin-bottom: 1rem; - background: var(--bg-primary); - border-radius: 0px; - border: 1px solid var(--border-color); -} - -.search-box { - flex: 1 1 200px; - min-width: 150px; -} - -.search-box form { - display: flex; - gap: 0.5rem; -} - -.search-box input[type="text"] { - flex: 1; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--bg-secondary); - color: var(--text-primary); - font-size: 0.9rem; - min-width: 100px; -} - -.search-box input[type="text"]:focus { - outline: none; +.preview-btn:hover { + background: var(--accent-blue); + color: white; border-color: var(--accent-blue); } -.search-box button { - padding: 0.5rem 1rem; +.copy-btn:hover { background: var(--accent-blue); color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - white-space: nowrap; + border-color: var(--accent-blue); } -.search-box button:hover { +.download-btn:hover { background: var(--accent-green); -} - -.sort-buttons { - display: flex; - align-items: center; - gap: 0.3rem; - flex-wrap: wrap; -} - -.sort-label { - font-size: 0.8rem; - color: var(--text-secondary); - font-weight: 500; - margin-right: 0.2rem; - white-space: nowrap; -} - -.sort-btn { - padding: 0.4rem 0.75rem; - background: var(--bg-secondary); - color: var(--text-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; - text-decoration: none; - font-size: 0.8rem; - transition: all 0.2s ease; - white-space: nowrap; -} - -.sort-btn:hover { - background: var(--hover-bg); - border-color: var(--accent-blue); -} - -.sort-btn.active { - background: var(--accent-blue); color: white; - border-color: var(--accent-blue); + border-color: var(--accent-green); } -/* ================================================ - Стили для формы ввода пароля - ================================================ */ +/* ============================================ + СТАТИСТИКА + ============================================ */ +.stats { + text-align: center; + color: var(--text-secondary); + margin-top: 2rem; + padding: 1rem 1.25rem; + border-top: 1px solid var(--border-color); + font-size: 0.9rem; + background: var(--bg-primary); +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--text-secondary); +} + +.empty-state .icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* ============================================ + ПАРОЛЬНАЯ ФОРМА + ============================================ */ .password-form-container { display: flex; justify-content: center; @@ -584,14 +615,13 @@ li:hover { text-decoration: underline; } -/* Стиль для защищенной папки */ .protected-folder { position: relative; } -/* ================================================ - Модальное окно предпросмотра - ================================================ */ +/* ============================================ + МОДАЛКА + ============================================ */ .preview-modal { display: none; position: fixed; @@ -600,31 +630,42 @@ li:hover { top: 0; width: 100%; height: 100%; - background-color: rgba(0,0,0,0.8); + background-color: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); overflow: auto; } +.preview-modal.open { + display: flex !important; + align-items: center; + justify-content: center; +} + .preview-content { background: var(--bg-secondary); - margin: 2% auto; - padding: 0; - border-radius: 8px; - box-shadow: 0 10px 30px rgba(0,0,0,0.3); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; - transition: all 0.3s ease; + max-width: 90vw; + max-height: 90vh; + width: auto; + height: auto; + min-width: 320px; + min-height: 200px; } .preview-header { display: flex; justify-content: space-between; align-items: center; - padding: 1rem; + padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); background: var(--bg-primary); flex-shrink: 0; + gap: 1rem; } .preview-title { @@ -635,38 +676,37 @@ li:hover { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding-right: 1rem; + font-size: 0.95rem; } .close-preview { - background: #dc3545; - color: white; + background: transparent; + color: var(--text-secondary); border: none; - border-radius: 4px; - width: 36px; - height: 36px; + border-radius: 6px; + width: 34px; + height: 34px; cursor: pointer; - font-size: 1.2rem; + font-size: 1.4rem; flex-shrink: 0; display: flex; align-items: center; justify-content: center; - transition: background 0.2s ease; + transition: all 0.15s ease; } .close-preview:hover { - background: #c82333; + background: #dc3545; + color: white; } .preview-iframe { flex-grow: 1; border: none; - background: white; + background: var(--bg-secondary); min-height: 0; -} - -.preview-iframe.pdf { - background: var(--bg-primary); + width: 100%; + height: 100%; } .preview-image-container { @@ -674,7 +714,7 @@ li:hover { align-items: center; justify-content: center; flex-grow: 1; - padding: 10px; + padding: 1rem; background: var(--bg-primary); overflow: hidden; width: 100%; @@ -687,28 +727,130 @@ li:hover { width: auto; height: auto; object-fit: contain; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0,0,0,0.2); + border-radius: 4px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); display: block; } -.preview-modal::-webkit-scrollbar { - width: 8px; +/* Форма пароля в модалке */ +#passwordFormContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + flex-grow: 1; + min-height: 300px; } -.preview-modal::-webkit-scrollbar-track { - background: rgba(255,255,255,0.1); +#passwordFormContainer > div { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 2rem; + max-width: 400px; + width: 100%; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } -.preview-modal::-webkit-scrollbar-thumb { - background: rgba(255,255,255,0.3); - border-radius: 4px; +/* ============================================ + ФУТЕР — НЕ УВЕЛИЧИВАЕМ + ============================================ */ +.footer { + background: var(--bg-primary); + border-top: 1px solid var(--border-color); + padding: 1.5rem 1rem; + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; } -.preview-modal::-webkit-scrollbar-thumb:hover { - background: rgba(255,255,255,0.5); +.footer a { + color: var(--accent-blue); + text-decoration: none; + font-weight: 500; + transition: opacity 0.15s ease; } +.footer a:hover { + opacity: 0.7; +} + +/* ============================================ + ТЕМА И СКРОЛЛ + ============================================ */ +.theme-toggle { + position: fixed; + top: 12px; + right: 12px; + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 50%; + width: 48px; + height: 48px; + font-size: 1.3rem; + cursor: pointer; + z-index: 1000; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + color: white; +} + +.dark-theme .theme-toggle { + background: rgba(0,0,0,0.3); + border-color: rgba(255,255,255,0.1); + color: var(--text-primary); +} + +.theme-toggle:hover { + transform: scale(1.1); + background: rgba(255,255,255,0.25); +} + +.dark-theme .theme-toggle:hover { + background: rgba(0,0,0,0.4); +} + +.scroll-top { + position: fixed; + bottom: 20px; + right: 20px; + width: 48px; + height: 48px; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 50%; + font-size: 1.3rem; + cursor: pointer; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 2px 12px rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; +} + +.scroll-top.visible { + opacity: 1; + visibility: visible; +} + +.scroll-top:hover { + background: var(--accent-green); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} + +/* ============================================ + УВЕДОМЛЕНИЕ + ============================================ */ #copyNotification { position: fixed; bottom: 80px; @@ -725,243 +867,521 @@ li:hover { opacity: 1; } -/* ================================================ - Адаптивность - ================================================ */ +/* ============================================ + АДАПТИВ — ПЛАНШЕТЫ + ============================================ */ @media (max-width: 768px) { - .preview-content { - margin: 2.5% auto; + .header { + padding: 1.25rem 1rem; } - .preview-header { - padding: 0.75rem; + .header h1 { + font-size: 1.5rem; } - .preview-title { - font-size: 0.9rem; + + .toolbar { + flex-direction: column; + align-items: stretch; + gap: 0.4rem; + padding: 0.6rem 1rem; } - .preview-image-container { - padding: 5px; + + .search-box { + flex: 1 1 100%; + min-width: 0; + } + + .search-box form { + flex-wrap: nowrap; + } + + .search-box input[type="text"] { + min-width: 60px; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + height: 34px; + } + + .search-box button { + font-size: 0.8rem; + padding: 0 0.9rem; + height: 34px; + } + + .settings-group { + flex-wrap: wrap; + justify-content: center; + min-height: 34px; + align-items: center; + } + + .view-btn, + .sort-btn { + font-size: 0.75rem; + padding: 0 0.7rem; + height: 34px; + } + + .settings-divider { + height: 26px; + } + + .file-controls { + gap: 0.2rem; + } + + .preview-btn, + .copy-btn, + .download-btn { + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + max-width: 28px; + max-height: 28px; + font-size: 0.7rem; + } + + .file-icon { + font-size: 1.2rem; + width: 24px; + } + + .file-name { + font-size: 0.85rem; } li { - padding: 0.5rem 0.25rem !important; - flex-wrap: nowrap !important; - } - .file-row { - flex-wrap: nowrap !important; - gap: 0.3rem !important; - } - .file-controls { - flex-wrap: nowrap !important; - gap: 0.15rem !important; + padding: 0.5rem 0.75rem; } + .file-meta.date { - min-width: 60px !important; + min-width: 60px; + width: 60px; } + .file-meta.size { - min-width: 50px !important; - font-size: 0.65rem !important; + min-width: 50px; + width: 50px; } - .toolbar { - flex-wrap: wrap !important; - gap: 0.5rem !important; - padding: 0.5rem !important; + + .breadcrumb { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; } - .search-box { - flex: 1 1 100% !important; - min-width: 0 !important; + + .preview-content { + max-width: 96vw; + max-height: 92vh; + min-width: auto; + border-radius: 8px; } - .sort-buttons { - flex-wrap: wrap !important; - justify-content: center !important; + + #passwordFormContainer { + padding: 1.5rem; + min-height: 250px; + } + + #passwordFormContainer > div { + padding: 1.5rem; } } +/* ============================================ + АДАПТИВ — МОБИЛКИ (ВСЁ УВЕЛИЧЕНО) + ============================================ */ @media (max-width: 480px) { + .header { + padding: 1.25rem 0.75rem; + } + .header h1 { + font-size: 1.6rem; + } + .header .subtitle { + font-size: 1rem; + } + .toolbar { - flex-direction: column !important; - align-items: stretch !important; - gap: 0.5rem !important; - padding: 0.5rem !important; + flex-direction: column; + align-items: stretch; + gap: 0.6rem; + padding: 0.75rem 0.75rem; } + .search-box { - width: 100% !important; - min-width: 0 !important; - flex: 1 1 auto !important; + width: 100%; + min-width: 0; } + .search-box form { - flex-wrap: nowrap !important; - width: 100% !important; + flex-wrap: nowrap; + width: 100%; + gap: 0.5rem; } + .search-box input[type="text"] { - flex: 1 1 auto !important; - min-width: 60px !important; - font-size: 0.85rem !important; - padding: 0.5rem 0.6rem !important; + font-size: 0.9rem; + padding: 0.6rem 0.8rem; + min-width: 60px; + height: 44px; + flex: 1; } + .search-box button { - flex: 0 0 auto !important; - font-size: 0.8rem !important; - padding: 0.4rem 0.7rem !important; - white-space: nowrap !important; + font-size: 0.9rem; + padding: 0 1rem; + height: 44px; + flex-shrink: 0; } - .sort-buttons { - display: flex !important; - flex-wrap: wrap !important; - justify-content: center !important; - gap: 0.3rem !important; - width: 100% !important; + + .settings-group { + gap: 0.3rem; + flex-wrap: wrap; + justify-content: center; + min-height: 40px; + align-items: center; } - .sort-label { - font-size: 0.65rem !important; - width: 100% !important; - text-align: center !important; - margin-right: 0 !important; + + .settings-label { + font-size: 0.7rem; + flex-shrink: 0; } + + .view-btn, .sort-btn { - flex: 0 1 auto !important; - font-size: 0.65rem !important; - padding: 0.25rem 0.4rem !important; + font-size: 0.8rem; + padding: 0 0.8rem; + height: 38px; + border-width: 1.5px; + flex-shrink: 0; + } + + .view-btn .icon, + .sort-btn .icon { + font-size: 0.85rem; + } + + /* ===== ФАЙЛОВАЯ СТРОКА — ФИКС ===== */ + .file-row { + gap: 0.3rem !important; + flex-wrap: nowrap !important; + } + + .file-link, + .dir-link { + gap: 0.3rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; + } + + .file-icon { + font-size: 1.4rem !important; + width: 32px !important; + min-width: 32px !important; + flex-shrink: 0 !important; + } + + .file-name { + font-size: 0.95rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; white-space: nowrap !important; } - .preview-btn, .copy-btn, .download-btn { - width: 26px !important; - height: 26px !important; - font-size: 0.65rem !important; - padding: 0 !important; + + .file-controls { + gap: 0.3rem !important; + flex-shrink: 0 !important; + margin-left: auto !important; } + + .file-meta.size { + font-size: 0.75rem !important; + min-width: 50px !important; + width: auto !important; + flex-shrink: 0 !important; + text-align: right !important; + } + .file-meta.date { display: none !important; } - .file-meta.size { - font-size: 0.55rem !important; - min-width: 35px !important; - } - .file-name { - font-size: 0.75rem !important; - } - .file-icon { - font-size: 1rem !important; - width: 18px !important; - margin-right: 0.3rem !important; - } - .file-controls { - gap: 0.1rem !important; + + .preview-btn, + .copy-btn, + .download-btn { + width: 38px !important; + height: 38px !important; + min-width: 38px !important; + min-height: 38px !important; + max-width: 38px !important; + max-height: 38px !important; + font-size: 0.9rem !important; + flex-shrink: 0 !important; + padding: 0 !important; + border-width: 1.5px !important; + margin: 0 !important; + border-radius: 8px !important; } + li { - padding: 0.3rem 0.15rem !important; + padding: 0.6rem 0.6rem !important; + min-height: 56px !important; } - .file-row { - gap: 0.15rem !important; - } - .theme-toggle { - top: 5px; - right: 5px; - width: 45px; - height: 45px; - font-size: 1.2rem; - } - .scroll-top { - bottom: 15px; - right: 15px; - width: 45px; - height: 45px; - font-size: 1.2rem; - } - .header { - padding: 1rem 0.5rem; - } - h1 { - font-size: 1.5rem; - padding: 0 0.5rem; - } - .content { - padding: 0.5rem; - } - .breadcrumb { - padding: 0.5rem; - font-size: 0.8rem; - } - .section-title { - font-size: 0.8rem; - padding: 0.4rem; - margin: 0.75rem 0 0.25rem 0; - } - .footer { - padding: 0.5rem; - font-size: 0.8rem; - } - .preview-content { - margin: 1% auto; - border-radius: 4px; - } - .preview-image-container { - padding: 2px; - } - .preview-image { - border-radius: 4px; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - } - #copyNotification { - bottom: 60px; - padding: 8px 16px; + + li.section-title { font-size: 0.85rem; + padding: 0.5rem 0.6rem; + } + + /* ===== ХЛЕБНЫЕ КРОШКИ ===== */ + .breadcrumb { + padding: 0.6rem 0.75rem; + font-size: 0.9rem; + gap: 0.3rem; + } + .breadcrumb a { + padding: 0.2rem 0.3rem; + } + + /* ===== СТАТИСТИКА ===== */ + .stats { + font-size: 0.85rem; + padding: 0.75rem 0.6rem; + } + + /* ===== ТЕМА И СКРОЛЛ ===== */ + .theme-toggle { + top: 8px; + right: 8px; + width: 48px; + height: 48px; + font-size: 1.3rem; + } + + .scroll-top { + bottom: 20px; + right: 20px; + width: 48px; + height: 48px; + font-size: 1.3rem; + } + + /* ===== ФУТЕР (НЕ УВЕЛИЧИВАЕМ) ===== */ + .footer { + padding: 1rem 0.75rem; + font-size: 0.8rem; + } + + /* ===== МОДАЛКА ===== */ + .preview-content { + max-width: 98vw; + max-height: 95vh; + border-radius: 8px; + min-height: 150px; + } + + .preview-header { + padding: 0.6rem 0.8rem; + } + + .preview-title { + font-size: 0.9rem; + } + + .close-preview { + width: 34px; + height: 34px; + font-size: 1.2rem; + } + + .preview-image-container { + padding: 0.5rem; + } + + #passwordFormContainer { + padding: 1rem; + min-height: 200px; + } + + #passwordFormContainer > div { + padding: 1.5rem 1rem; + border-radius: 8px; + } + + /* ===== ФОРМА ПАРОЛЯ ===== */ + .password-form { + padding: 1.5rem 1rem; + } + .password-form .lock-icon { + font-size: 2.5rem; + } + .password-form h2 { + font-size: 1.2rem; + } + .password-form p { + font-size: 0.9rem; + } + .password-form input[type="password"] { + font-size: 1rem; + padding: 0.7rem 0.8rem; + height: 48px; + } + .password-form button { + font-size: 1rem; + padding: 0.7rem; + height: 48px; } } +/* ============================================ + АДАПТИВ — ОЧЕНЬ МАЛЕНЬКИЕ ЭКРАНЫ + ============================================ */ @media (max-width: 360px) { - .container { - min-width: 100%; - margin: 0; - } - .file-name { - font-size: 0.7rem !important; - } - .file-meta.size { - font-size: 0.5rem !important; - min-width: 28px !important; - } - .preview-btn, .copy-btn, .download-btn { - width: 20px !important; - height: 20px !important; - font-size: 0.5rem !important; + .header h1 { + font-size: 1.3rem; } + .search-box input[type="text"] { - font-size: 0.75rem !important; - padding: 0.3rem 0.4rem !important; - min-width: 40px !important; + font-size: 0.8rem; + height: 40px; + padding: 0.4rem 0.6rem; } .search-box button { - font-size: 0.7rem !important; - padding: 0.3rem 0.5rem !important; + font-size: 0.8rem; + height: 40px; + padding: 0 0.7rem; } + + .view-btn, .sort-btn { - font-size: 0.6rem !important; - padding: 0.2rem 0.3rem !important; + font-size: 0.7rem; + padding: 0 0.5rem; + height: 34px; + } + + .file-icon { + font-size: 1.2rem !important; + width: 28px !important; + min-width: 28px !important; + } + + .file-name { + font-size: 0.85rem !important; + } + + .preview-btn, + .copy-btn, + .download-btn { + width: 34px !important; + height: 34px !important; + min-width: 34px !important; + min-height: 34px !important; + max-width: 34px !important; + max-height: 34px !important; + font-size: 0.8rem !important; + border-radius: 6px !important; + } + + .file-meta.size { + font-size: 0.65rem !important; + min-width: 40px !important; + } + + li { + padding: 0.4rem 0.4rem !important; + min-height: 48px !important; + } + + .file-row { + gap: 0.3rem !important; + } + + .theme-toggle { + width: 40px; + height: 40px; + font-size: 1.1rem; + top: 6px; + right: 6px; + } + + .scroll-top { + width: 40px; + height: 40px; + font-size: 1.1rem; + bottom: 16px; + right: 16px; + } + + .password-form input[type="password"] { + height: 42px; + font-size: 0.9rem; + } + .password-form button { + height: 42px; + font-size: 0.9rem; } } -/* ================================================ - ПЕРЕОПРЕДЕЛЕНИЕ МОДАЛКИ (фикс темы) - ================================================ */ -.preview-content { - background: var(--modal-bg) !important; - color: var(--modal-text) !important; - border: 1px solid var(--modal-border) !important; +/* ===== ФАЙЛЫ — МОБИЛКИ ===== */ +.file-row { + gap: 0.3rem !important; } -.preview-header { - background: var(--modal-header-bg) !important; - border-bottom: 1px solid var(--modal-border) !important; +.file-link, +.dir-link { + gap: 0.3rem !important; + flex: 1 1 auto !important; } -.preview-title { - color: var(--modal-text) !important; +.file-icon { + font-size: 1.4rem !important; + width: 32px !important; + min-width: 32px !important; + max-width: 32px !important; + flex-shrink: 0 !important; } -.preview-iframe { - background: var(--bg-primary) !important; +.file-name { + font-size: 0.95rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; } -.preview-image-container { - background: var(--bg-primary) !important; +.file-link .file-name { + font-weight: 500 !important; } + +.dir-link .file-name { + font-weight: 600 !important; +} + +.file-controls { + gap: 0.3rem !important; + flex-shrink: 0 !important; + margin-left: auto !important; +} + +.file-meta.size { + font-size: 0.75rem !important; + min-width: 50px !important; + width: auto !important; + flex-shrink: 0 !important; +} + +.file-meta.date { + display: none !important; +} + +.preview-btn, +.copy-btn, +.download-btn { + width: 38px !important; + height: 38px !important; + min-width: 38px !important; + min-height: 38px !important; + max-width: 38px !important; + max-height: 38px !important; + font-size: 0.9rem !important; + flex-shrink: 0 !important; +} \ No newline at end of file diff --git a/template.html b/template.html index 5ec6651..8d225ae 100644 --- a/template.html +++ b/template.html @@ -130,14 +130,21 @@
@@ -154,7 +161,55 @@
- + \ No newline at end of file