+#include "render.h"
+#include "config.h"
+#include "auth.h"
+#include "fs.h"
+#include "utils.h"
+
+extern query_params_t g_params;
+
+void buffer_init(buffer_t *buf) {
+ buf->capacity = 65536;
+ buf->data = malloc(buf->capacity);
+ buf->size = 0;
+}
+
+void buffer_append(buffer_t *buf, const char *str) {
+ size_t len = strlen(str);
+ if (buf->size + len >= buf->capacity) {
+ buf->capacity *= 2;
+ buf->data = realloc(buf->data, buf->capacity);
+ }
+ memcpy(buf->data + buf->size, str, len);
+ buf->size += len;
+}
+
+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, "
🏠 Главная ");
+
+ if (display_path && display_path[0]) {
+ char temp_path[1024] = "";
+ char encoded[2048];
+ char *path_copy = strdup(display_path);
+ char *token = strtok(path_copy, "/");
+ while (token) {
+ buffer_append(buf, " / ");
+ if (temp_path[0]) strcat(temp_path, "/");
+ strcat(temp_path, token);
+ url_encode(temp_path, encoded, sizeof(encoded));
+ char safe_token[512];
+ html_escape(token, safe_token, sizeof(safe_token));
+
+ buffer_append(buf, "
");
+ buffer_append(buf, safe_token);
+ buffer_append(buf, " ");
+
+ token = strtok(NULL, "/");
+ }
+ free(path_copy);
+ }
+ buffer_append(buf, "\n
\n");
+}
+
+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");
+ return;
+ }
+
+ char encoded_path[1024];
+ url_encode(folder_name, encoded_path, sizeof(encoded_path));
+
+ buffer_append(buf, "\n");
+}
+
+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 = 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 *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;
+ }
+
+ 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 (!is_root && !has_valid_token && display_path && display_path[0]) {
+ char *last_slash = strrchr(display_path, '/');
+ 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);
+ 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') {
+ int result_count = 0;
+ search_recursive(g_config.base_path, "", g_params.search, entries, &result_count, g_config.max_entries);
+ if (result_count == 0) {
+ buffer_append(buf, "\n");
+ buffer_append(buf, "
🔍
\n");
+ buffer_append(buf, "
Ничего не найдено \n");
+ buffer_append(buf, "
По запросу \"");
+ char safe_search[512];
+ html_escape(g_params.search, safe_search, sizeof(safe_search));
+ buffer_append(buf, safe_search);
+ buffer_append(buf, "\" ничего не найдено
\n");
+ buffer_append(buf, "
\n");
+ free(entries);
+ return;
+ }
+ entry_count = result_count;
+ is_recursive_search = 1;
+ for (int i = 0; i < entry_count; i++) {
+ if (entries[i].is_dir) {
+ dir_count++;
+ total_size += entries[i].dir_size;
+ } else {
+ file_count++;
+ total_size += entries[i].size;
+ }
+ }
+ } else {
+ dir = opendir(base_path);
+ if (!dir) {
+ char alt_path[1024];
+ snprintf(alt_path, sizeof(alt_path), "%s", g_config.base_path);
+ if (display_path[0]) {
+ char *encoded = strdup(display_path);
+ url_decode_enhanced(encoded, alt_path + strlen(alt_path), sizeof(alt_path) - strlen(alt_path));
+ free(encoded);
+ }
+ 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");
+ free(entries);
+ return;
+ }
+ }
+ while ((entry = readdir(dir)) != NULL && entry_count < g_config.max_entries) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
+ int hidden = 0;
+ for (int i = 0; i < g_config.hidden_dirs_count; i++) {
+ if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; }
+ }
+ for (int i = 0; i < g_config.hidden_files_count; i++) {
+ if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; }
+ }
+ if (entry->d_name[0] == '.') hidden = 1;
+ if (hidden) continue;
+ snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name);
+ if (stat(full_path, &file_stat) == 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;
+ entries[entry_count].mtime = file_stat.st_mtime;
+ entries[entry_count].file_count = count_files_in_dir(full_path);
+ entries[entry_count].dir_size = get_dir_size(full_path);
+ total_size += entries[entry_count].dir_size;
+ if (is_folder_protected(entry->d_name)) {
+ strcpy(entries[entry_count].icon, "🔒");
+ entries[entry_count].is_protected = 1;
+ } else {
+ strcpy(entries[entry_count].icon, "📁");
+ entries[entry_count].is_protected = 0;
+ }
+ char new_path[2048];
+ snprintf(new_path, sizeof(new_path), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name);
+ url_encode(new_path, entries[entry_count].encoded_path, sizeof(entries[0].encoded_path));
+ dir_count++;
+ } else if (S_ISREG(file_stat.st_mode)) {
+ entries[entry_count].is_dir = 0;
+ entries[entry_count].size = file_stat.st_size;
+ entries[entry_count].mtime = file_stat.st_mtime;
+ entries[entry_count].file_count = 0;
+ entries[entry_count].dir_size = 0;
+ strcpy(entries[entry_count].icon, get_file_icon(entry->d_name));
+ entries[entry_count].is_protected = 0;
+ char encoded_filename[1024];
+ url_encode(entry->d_name, encoded_filename, sizeof(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 {
+ continue;
+ }
+ entry_count++;
+ }
+ }
+ closedir(dir);
+ if (entry_count == 0) {
+ buffer_append(buf, "\n");
+ buffer_append(buf, "
📄
\n");
+ buffer_append(buf, "
Здесь пусто \n");
+ buffer_append(buf, "
В этой директории нет файлов или папок
\n");
+ buffer_append(buf, "
\n");
+ free(entries);
+ 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");
+
+ if (is_recursive_search) {
+ buffer_append(buf, "");
+ buffer_append(buf, "🔍 Результаты поиска по всему сайту: ");
+ char tmp[32];
+ snprintf(tmp, sizeof(tmp), "%d", entry_count);
+ buffer_append(buf, tmp);
+ buffer_append(buf, " файлов/папок найдено
\n");
+ }
+
+ buffer_append(buf, "\n");
+ int has_dirs = 0;
+ for (int i = 0; i < entry_count; i++) if (entries[i].is_dir) { has_dirs = 1; break; }
+ if (has_dirs) {
+ int display_dir_count = 0;
+ for (int i = 0; i < entry_count; i++) if (entries[i].is_dir) display_dir_count++;
+ char section_title[128];
+ snprintf(section_title, sizeof(section_title), " 📁 Папки (%d) \n", display_dir_count);
+ buffer_append(buf, section_title);
+ for (int i = 0; i < entry_count; i++) {
+ if (entries[i].is_dir) {
+ buffer_append(buf, " \n");
+ if (entries[i].is_protected) {
+ buffer_append(buf, " \n");
+ if (token_part[0]) {
+ buffer_append(buf, "
\n");
+ } else {
+ buffer_append(buf, " \n");
+ }
+ buffer_append(buf, " 📁 \n");
+ buffer_append(buf, " ");
+ if (is_recursive_search) {
+ char safe_url[512];
+ html_escape(entries[i].file_url, safe_url, sizeof(safe_url));
+ buffer_append(buf, safe_url);
+ } else {
+ char safe_name2[512];
+ html_escape(entries[i].name, safe_name2, sizeof(safe_name2));
+ buffer_append(buf, safe_name2);
+ }
+ 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");
+ } else {
+ buffer_append(buf, " \n");
+ buffer_append(buf, "
\n");
+ buffer_append(buf, " ");
+ buffer_append(buf, entries[i].icon);
+ buffer_append(buf, " \n");
+ buffer_append(buf, " ");
+ if (is_recursive_search) {
+ char safe_url[512];
+ html_escape(entries[i].file_url, safe_url, sizeof(safe_url));
+ buffer_append(buf, safe_url);
+ } else {
+ char safe_name4[512];
+ html_escape(entries[i].name, safe_name4, sizeof(safe_name4));
+ buffer_append(buf, safe_name4);
+ }
+ buffer_append(buf, " \n");
+ buffer_append(buf, " \n");
+ buffer_append(buf, "
\n");
+ buffer_append(buf, " 🔗 \n");
+ if (entries[i].dir_size > 0) {
+ char size_str[32];
+ format_size(entries[i].dir_size, size_str);
+ buffer_append(buf, " ");
+ buffer_append(buf, size_str);
+ buffer_append(buf, " \n");
+ }
+ buffer_append(buf, "
\n");
+ buffer_append(buf, "
\n");
+ }
+ buffer_append(buf, " \n");
+ }
+ }
+ }
+ int has_files = 0;
+ for (int i = 0; i < entry_count; i++) if (!entries[i].is_dir) { has_files = 1; break; }
+ if (has_files) {
+ int display_file_count = 0;
+ for (int i = 0; i < entry_count; i++) if (!entries[i].is_dir) display_file_count++;
+ char section_title[128];
+ snprintf(section_title, sizeof(section_title), " 📄 Файлы (%d) \n", display_file_count);
+ buffer_append(buf, section_title);
+ for (int i = 0; i < entry_count; i++) {
+ if (!entries[i].is_dir) {
+ char size_str[32];
+ char date_str[64];
+ format_size(entries[i].size, size_str);
+ format_date(entries[i].mtime, date_str, sizeof(date_str));
+ buffer_append(buf, " \n");
+ buffer_append(buf, " \n");
+ char file_link[2048];
+ if (is_recursive_search) {
+ snprintf(file_link, sizeof(file_link), "/%s", entries[i].file_url);
+ } else {
+ snprintf(file_link, sizeof(file_link), "/%s", entries[i].file_url);
+ }
+ buffer_append(buf, "
\n");
+ buffer_append(buf, " ");
+ buffer_append(buf, entries[i].icon);
+ buffer_append(buf, " \n");
+ buffer_append(buf, " ");
+ if (is_recursive_search) {
+ char safe_url[512];
+ html_escape(entries[i].file_url, safe_url, sizeof(safe_url));
+ buffer_append(buf, safe_url);
+ } else {
+ char safe_name5[512];
+ html_escape(entries[i].name, safe_name5, sizeof(safe_name5));
+ buffer_append(buf, safe_name5);
+ }
+ buffer_append(buf, " \n");
+ buffer_append(buf, " \n");
+ buffer_append(buf, "
\n");
+ if (is_previewable(entries[i].name)) {
+ buffer_append(buf, " 👁 \n");
+ }
+ buffer_append(buf, " ⬇️ \n");
+ buffer_append(buf, " 🔗 \n");
+ buffer_append(buf, " ");
+ buffer_append(buf, date_str);
+ buffer_append(buf, " \n");
+ buffer_append(buf, " ");
+ buffer_append(buf, size_str);
+ buffer_append(buf, " \n");
+ buffer_append(buf, "
\n");
+ buffer_append(buf, "
\n");
+ buffer_append(buf, " \n");
+ }
+ }
+ }
+ buffer_append(buf, " \n");
+ if (is_recursive_search) {
+ char stats[256];
+ snprintf(stats, sizeof(stats),
+ "\n Найдено: папок %d | файлов %d\n
\n",
+ dir_count, file_count);
+ buffer_append(buf, stats);
+ } else {
+ char total_size_str[32];
+ format_size(total_size, total_size_str);
+ char stats[256];
+ snprintf(stats, sizeof(stats),
+ "\n Папки: %d | Файлы: %d | Общий размер: %s\n
\n",
+ dir_count, file_count, total_size_str);
+ 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];
+ while (fgets(line, sizeof(line), file)) {
+ if (strstr(line, "")) {
+ print_breadcrumb(&output, display_path);
+ } else if (strstr(line, "")) {
+ print_content(&output, base_path, display_path);
+ } else {
+ buffer_append(&output, line);
+ }
+ }
+ 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/www/cgi-bin/src/render.h b/www/cgi-bin/src/render.h
new file mode 100644
index 0000000..0b38b6b
--- /dev/null
+++ b/www/cgi-bin/src/render.h
@@ -0,0 +1,18 @@
+#ifndef RENDER_H
+#define RENDER_H
+
+#include "fs.h"
+
+typedef struct {
+ char *data;
+ size_t size;
+ size_t capacity;
+} buffer_t;
+
+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/www/cgi-bin/src/status.c b/www/cgi-bin/src/status.c
new file mode 100644
index 0000000..42a82ba
--- /dev/null
+++ b/www/cgi-bin/src/status.c
@@ -0,0 +1,97 @@
+// /home/romkazvo/www/cgi-bin/src/status.c
+#include
+#include
+#include
+#include
+
+long get_nginx_memory() {
+ long total_kb = 0;
+ char command[] = "ps -o rss= -C nginx 2>/dev/null";
+
+ FILE *ps = popen(command, "r");
+ if (!ps) return 0;
+
+ char line[64];
+ while (fgets(line, sizeof(line), ps)) {
+ long kb;
+ if (sscanf(line, "%ld", &kb) == 1) {
+ total_kb += kb;
+ }
+ }
+ pclose(ps);
+
+ return total_kb;
+}
+
+int get_process_memory(const char *process_name, long *memory_kb) {
+ char command[256];
+ snprintf(command, sizeof(command), "ps -o rss= -C %s 2>/dev/null | head -1", process_name);
+
+ FILE *ps = popen(command, "r");
+ if (!ps) return 0;
+
+ int result = fscanf(ps, "%ld", memory_kb);
+ pclose(ps);
+
+ 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();
+
+ 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/www/cgi-bin/src/style.c b/www/cgi-bin/src/style.c
new file mode 100644
index 0000000..818e8c1
--- /dev/null
+++ b/www/cgi-bin/src/style.c
@@ -0,0 +1,36 @@
+#include
+#include
+#include
+#include
+
+int main() {
+ char css_path[1024] = "/home/romkazvo/www/cgi-bin/style.css";
+ struct stat st;
+
+ if (stat(css_path, &st) != 0) {
+ printf("Status: 404 Not Found\n");
+ printf("Content-type: text/plain\n\n");
+ printf("CSS file not found\n");
+ return 1;
+ }
+
+ printf("Content-type: text/css\n");
+ printf("Cache-Control: public, max-age=3600\n\n");
+
+ FILE *css_file = fopen(css_path, "r");
+ if (!css_file) {
+ printf("Status: 500 Internal Server Error\n");
+ printf("Content-type: text/plain\n\n");
+ printf("Cannot open CSS file\n");
+ return 1;
+ }
+
+ char buffer[4096];
+ size_t bytes_read;
+ while ((bytes_read = fread(buffer, 1, sizeof(buffer), css_file)) > 0) {
+ fwrite(buffer, 1, bytes_read, stdout);
+ }
+
+ fclose(css_file);
+ return 0;
+}
diff --git a/www/cgi-bin/src/utils.c b/www/cgi-bin/src/utils.c
new file mode 100644
index 0000000..b7caffe
--- /dev/null
+++ b/www/cgi-bin/src/utils.c
@@ -0,0 +1,98 @@
+#include
+#include
+#include
+#include
+#include
+#include "utils.h"
+
+char* my_strcasestr(const char *haystack, const char *needle) {
+ if (!haystack || !needle || *needle == '\0') return (char*)haystack;
+ size_t needle_len = strlen(needle);
+ while (*haystack) {
+ if (strncasecmp(haystack, needle, needle_len) == 0) return (char*)haystack;
+ haystack++;
+ }
+ return NULL;
+}
+
+void html_escape(const char *src, char *dst, size_t dst_size) {
+ if (!src || !dst || dst_size == 0) return;
+ char *p = dst;
+ size_t i = 0;
+ while (*src && i < dst_size - 6) {
+ switch (*src) {
+ case '<': strcpy(p, "<"); p += 4; i += 4; break;
+ case '>': strcpy(p, ">"); p += 4; i += 4; break;
+ case '"': strcpy(p, """); p += 6; i += 6; break;
+ case '&': strcpy(p, "&"); p += 5; i += 5; break;
+ default: *p++ = *src; i++; break;
+ }
+ src++;
+ }
+ *p = '\0';
+}
+
+void format_size(long long size, char* buffer) {
+ if (size < 1024) snprintf(buffer, 32, "%lld B", size);
+ else if (size < 1024 * 1024) snprintf(buffer, 32, "%.1f KB", size / 1024.0);
+ else if (size < 1024 * 1024 * 1024) snprintf(buffer, 32, "%.1f MB", size / (1024.0 * 1024.0));
+ else snprintf(buffer, 32, "%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
+}
+
+void format_date(time_t mtime, char* buffer, size_t size) {
+ struct tm *tm_info = localtime(&mtime);
+ strftime(buffer, size, "%d.%m.%Y", tm_info);
+}
+
+void url_encode(const char *src, char *dst, size_t dst_size) {
+ static const char *hex = "0123456789ABCDEF";
+ char *p = dst;
+ size_t i = 0;
+ while (*src && i < dst_size - 3) {
+ unsigned char c = (unsigned char)*src;
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
+ (c >= '0' && c <= '9') || strchr("-_.~", c)) {
+ *p++ = c; i++;
+ } else if (c == ' ') {
+ *p++ = '%'; *p++ = '2'; *p++ = '0'; i += 3;
+ } else {
+ *p++ = '%'; *p++ = hex[(c >> 4) & 0xF]; *p++ = hex[c & 0xF]; i += 3;
+ }
+ src++;
+ }
+ *p = '\0';
+}
+
+void url_decode_enhanced(const char *src, char *dst, size_t dst_size) {
+ char *p = dst;
+ size_t decoded_len = 0;
+ while (*src && decoded_len < dst_size - 1) {
+ if (*src == '%') {
+ if (src[1] && src[2] && isxdigit(src[1]) && isxdigit(src[2])) {
+ char hex[3] = {src[1], src[2], '\0'};
+ unsigned char c = (unsigned char)strtol(hex, NULL, 16);
+ if (c >= 0x80 || c >= 0x20 || c == 0x0A || c == 0x0D) {
+ *p++ = c;
+ decoded_len++;
+ } else {
+ *p++ = '_';
+ decoded_len++;
+ }
+ src += 3;
+ } else {
+ // ============================================
+ // ФИКС: если % не валидный — просто пропускаем его
+ // ============================================
+ src++;
+ }
+ } else if (*src == '+') {
+ *p++ = ' ';
+ decoded_len++;
+ src++;
+ } else {
+ *p++ = *src++;
+ decoded_len++;
+ }
+ }
+ *p = '\0';
+}
\ No newline at end of file
diff --git a/www/cgi-bin/src/utils.h b/www/cgi-bin/src/utils.h
new file mode 100644
index 0000000..0301302
--- /dev/null
+++ b/www/cgi-bin/src/utils.h
@@ -0,0 +1,13 @@
+#ifndef UTILS_H
+#define UTILS_H
+
+#include
+
+void url_encode(const char *src, char *dst, size_t dst_size);
+void url_decode_enhanced(const char *src, char *dst, size_t dst_size);
+void html_escape(const char *src, char *dst, size_t dst_size);
+char* my_strcasestr(const char *haystack, const char *needle);
+void format_size(long long size, char* buffer);
+void format_date(time_t mtime, char* buffer, size_t size);
+
+#endif
diff --git a/www/cgi-bin/style.css b/www/cgi-bin/style.css
new file mode 100755
index 0000000..98a550f
--- /dev/null
+++ b/www/cgi-bin/style.css
@@ -0,0 +1,1387 @@
+/* ============================================
+ БАЗА
+ ============================================ */
+:root {
+ --bg-primary: #f8f9fa;
+ --bg-secondary: #ffffff;
+ --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: #1e1e1e;
+ --bg-secondary: #2d2d2d;
+ --bg-header: linear-gradient(135deg, #1e1e1e, #2d2d2d);
+ --text-primary: #e0e0e0;
+ --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);
+ line-height: 1.6;
+ min-height: 100vh;
+ transition: background 0.25s ease, color 0.25s ease;
+}
+
+body.loaded * {
+ 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);
+ 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: 1100px;
+ margin: 16px auto;
+ min-height: auto;
+ border-radius: var(--radius);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+ }
+}
+
+/* ============================================
+ ШАПКА
+ ============================================ */
+.header {
+ background: var(--bg-header);
+ color: white;
+ padding: 2rem 1.5rem;
+ text-align: center;
+ position: relative;
+}
+
+.header h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ margin: 0;
+ letter-spacing: -0.5px;
+}
+
+.header .subtitle {
+ opacity: 0.85;
+ font-size: 1.05rem;
+ margin-top: 0.25rem;
+ font-weight: 400;
+}
+
+/* ============================================
+ ОПИСАНИЕ (НЕ УВЕЛИЧИВАЕМ)
+ ============================================ */
+.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;
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ flex-wrap: nowrap;
+}
+
+.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: 0.75rem;
+ flex-wrap: nowrap;
+ width: 100%;
+}
+
+.file-link,
+.dir-link {
+ display: flex;
+ align-items: center;
+ 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.3rem;
+ flex-shrink: 0;
+ flex-wrap: nowrap;
+ margin-left: auto;
+}
+
+.file-meta {
+ color: var(--text-muted);
+ 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: 80px;
+ width: 80px;
+}
+
+.file-meta.size {
+ min-width: 60px;
+ width: 60px;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+/* Кнопки — фиксированный размер */
+.preview-btn,
+.copy-btn,
+.download-btn {
+ background: transparent;
+ 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.8rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ 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 {
+ opacity: 1;
+ transform: scale(1.05);
+}
+
+.preview-btn:hover {
+ background: var(--accent-blue);
+ color: white;
+ border-color: var(--accent-blue);
+}
+
+.copy-btn:hover {
+ background: var(--accent-blue);
+ color: white;
+ border-color: var(--accent-blue);
+}
+
+.download-btn:hover {
+ background: var(--accent-green);
+ color: white;
+ 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;
+ align-items: center;
+ min-height: 400px;
+ padding: 2rem;
+}
+
+.password-form {
+ 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.1);
+}
+
+.password-form .lock-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ display: block;
+}
+
+.password-form h2 {
+ font-size: 1.3rem;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+}
+
+.password-form p {
+ color: var(--text-secondary);
+ margin-bottom: 1.5rem;
+ font-size: 0.95rem;
+}
+
+.password-form p strong {
+ color: var(--text-primary);
+ word-break: break-all;
+}
+
+.password-form form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.password-form input[type="password"] {
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 1rem;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ transition: border-color 0.2s ease;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.password-form input[type="password"]:focus {
+ outline: none;
+ border-color: var(--accent-blue);
+}
+
+.password-form button {
+ padding: 0.75rem;
+ background: var(--accent-blue);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.password-form button:hover {
+ background: var(--accent-green);
+}
+
+.password-form .error-msg {
+ color: #dc3545;
+ font-size: 0.9rem;
+ min-height: 1.5rem;
+ margin: 0.25rem 0;
+}
+
+.password-form .back-link {
+ display: inline-block;
+ margin-top: 0.75rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.9rem;
+}
+
+.password-form .back-link:hover {
+ color: var(--accent-blue);
+ text-decoration: underline;
+}
+
+.protected-folder {
+ position: relative;
+}
+
+/* ============================================
+ МОДАЛКА
+ ============================================ */
+.preview-modal {
+ display: none;
+ position: fixed;
+ z-index: 2000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ 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);
+ border-radius: 12px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ 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: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ flex-shrink: 0;
+ gap: 1rem;
+}
+
+.preview-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 0.95rem;
+}
+
+.close-preview {
+ background: transparent;
+ color: var(--text-secondary);
+ border: none;
+ border-radius: 6px;
+ width: 34px;
+ height: 34px;
+ cursor: pointer;
+ font-size: 1.4rem;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease;
+}
+
+.close-preview:hover {
+ background: #dc3545;
+ color: white;
+}
+
+.preview-iframe {
+ flex-grow: 1;
+ border: none;
+ background: var(--bg-secondary);
+ min-height: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.preview-image-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-grow: 1;
+ padding: 1rem;
+ background: var(--bg-primary);
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+}
+
+.preview-image {
+ max-width: 100%;
+ max-height: 100%;
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ border-radius: 4px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+ display: block;
+}
+
+/* Форма пароля в модалке */
+#passwordFormContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 2rem;
+ flex-grow: 1;
+ min-height: 300px;
+}
+
+#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);
+}
+
+/* ============================================
+ ФУТЕР — НЕ УВЕЛИЧИВАЕМ
+ ============================================ */
+.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;
+}
+
+.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;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--accent-green);
+ color: white;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-weight: 600;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ z-index: 3000;
+ transition: opacity 0.3s;
+ opacity: 1;
+}
+
+/* ============================================
+ АДАПТИВ — ПЛАНШЕТЫ
+ ============================================ */
+@media (max-width: 768px) {
+ .header {
+ padding: 1.25rem 1rem;
+ }
+ .header h1 {
+ font-size: 1.5rem;
+ }
+
+ .toolbar {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.4rem;
+ padding: 0.6rem 1rem;
+ }
+
+ .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.75rem;
+ }
+
+ .file-meta.date {
+ min-width: 60px;
+ width: 60px;
+ }
+
+ .file-meta.size {
+ min-width: 50px;
+ width: 50px;
+ }
+
+ .breadcrumb {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+ }
+
+ .preview-content {
+ max-width: 96vw;
+ max-height: 92vh;
+ min-width: auto;
+ border-radius: 8px;
+ }
+
+ #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;
+ align-items: stretch;
+ gap: 0.6rem;
+ padding: 0.75rem 0.75rem;
+ }
+
+ .search-box {
+ width: 100%;
+ min-width: 0;
+ }
+
+ .search-box form {
+ flex-wrap: nowrap;
+ width: 100%;
+ gap: 0.5rem;
+ }
+
+ .search-box input[type="text"] {
+ font-size: 0.9rem;
+ padding: 0.6rem 0.8rem;
+ min-width: 60px;
+ height: 44px;
+ flex: 1;
+ }
+
+ .search-box button {
+ font-size: 0.9rem;
+ padding: 0 1rem;
+ height: 44px;
+ flex-shrink: 0;
+ }
+
+ .settings-group {
+ gap: 0.3rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ min-height: 40px;
+ align-items: center;
+ }
+
+ .settings-label {
+ font-size: 0.7rem;
+ flex-shrink: 0;
+ }
+
+ .view-btn,
+ .sort-btn {
+ 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;
+ }
+
+ .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;
+ }
+
+ .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.6rem 0.6rem !important;
+ min-height: 56px !important;
+ }
+
+ 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) {
+ .header h1 {
+ font-size: 1.3rem;
+ }
+
+ .search-box input[type="text"] {
+ font-size: 0.8rem;
+ height: 40px;
+ padding: 0.4rem 0.6rem;
+ }
+ .search-box button {
+ font-size: 0.8rem;
+ height: 40px;
+ padding: 0 0.7rem;
+ }
+
+ .view-btn,
+ .sort-btn {
+ 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;
+ }
+}
+
+/* ===== ФАЙЛЫ — МОБИЛКИ ===== */
+.file-row {
+ gap: 0.3rem !important;
+}
+
+.file-link,
+.dir-link {
+ gap: 0.3rem !important;
+ flex: 1 1 auto !important;
+}
+
+.file-icon {
+ font-size: 1.4rem !important;
+ width: 32px !important;
+ min-width: 32px !important;
+ max-width: 32px !important;
+ flex-shrink: 0 !important;
+}
+
+.file-name {
+ font-size: 0.95rem !important;
+ flex: 1 1 auto !important;
+ min-width: 0 !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/www/cgi-bin/template.html b/www/cgi-bin/template.html
new file mode 100644
index 0000000..8d225ae
--- /dev/null
+++ b/www/cgi-bin/template.html
@@ -0,0 +1,546 @@
+
+
+
+
+
+ Trashbox - личная помоечка
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🌙
+
+
+
+
+ Личный уголок для обмена файлами. Заливаю сюда то, чем хочу поделиться, без всяких лимитов и сторонних сервисов.
+
+
+
+
+
+
+
+
+
+ ↑
+
+
+
+
+
+
\ No newline at end of file