diff --git a/config.ini b/config.ini new file mode 100755 index 0000000..b815aaf --- /dev/null +++ b/config.ini @@ -0,0 +1,20 @@ +# Trashbox CGI config + +[paths] +base_path = /home/romkazvo/www +template_path = /home/romkazvo/www/cgi-bin/template.html +passwd_file = /home/romkazvo/www/cgi-bin/.htpasswd + +[limits] +max_entries = 1000 + +[hidden] +dirs = cgi-bin,.git,.svn +files = .htpasswd,.htaccess,.gitignore + +[preview] +image = jpg,jpeg,png,gif,webp,bmp,svg,ico +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 diff --git a/index.c b/index.c old mode 100755 new mode 100644 index 98b02dc..5485e31 --- a/index.c +++ b/index.c @@ -8,11 +8,146 @@ #include #include #include +#include -#define MAX_ENTRIES 1000 -#define TEMPLATE_PATH "/home/romkazvo/www/cgi-bin/template.html" -#define OUTPUT_BUFFER_SIZE 65536 -#define PASSWD_FILE "/home/romkazvo/www/cgi-bin/.htpasswd" +// === КОНФИГУРАЦИЯ === + +typedef struct { + char base_path[1024]; + char template_path[1024]; + char passwd_file[1024]; + int max_entries; + char hidden_dirs[10][256]; + int hidden_dirs_count; + char hidden_files[10][256]; + int hidden_files_count; + char preview_image[512]; + char preview_text[512]; + char preview_audio[512]; + char preview_video[512]; + char preview_pdf[512]; +} config_t; + +config_t g_config; + +// Простой парсер INI (без зависимостей) +int load_config(const char *path, config_t *cfg) { + FILE *f = fopen(path, "r"); + if (!f) return -1; + + char line[512]; + char current_section[64] = ""; + + while (fgets(line, sizeof(line), f)) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0' || *p == '#' || *p == ';') continue; + + if (*p == '[') { + p++; + char *end = strchr(p, ']'); + if (end) { + *end = '\0'; + strncpy(current_section, p, sizeof(current_section) - 1); + current_section[sizeof(current_section) - 1] = '\0'; + } + continue; + } + + char *eq = strchr(p, '='); + if (!eq) continue; + + *eq = '\0'; + char *key = p; + char *value = eq + 1; + + char *end_key = key + strlen(key) - 1; + while (end_key > key && (*end_key == ' ' || *end_key == '\t')) { + *end_key = '\0'; + end_key--; + } + + while (*value == ' ' || *value == '\t') value++; + char *end_val = value + strlen(value) - 1; + while (end_val > value && (*end_val == ' ' || *end_val == '\t' || *end_val == '\r' || *end_val == '\n')) { + *end_val = '\0'; + end_val--; + } + + if (strcmp(current_section, "paths") == 0) { + if (strcmp(key, "base_path") == 0) strncpy(cfg->base_path, value, sizeof(cfg->base_path) - 1); + else if (strcmp(key, "template_path") == 0) strncpy(cfg->template_path, value, sizeof(cfg->template_path) - 1); + else if (strcmp(key, "passwd_file") == 0) strncpy(cfg->passwd_file, value, sizeof(cfg->passwd_file) - 1); + } + else if (strcmp(current_section, "limits") == 0) { + if (strcmp(key, "max_entries") == 0) cfg->max_entries = atoi(value); + } + else if (strcmp(current_section, "hidden") == 0) { + if (strcmp(key, "dirs") == 0) { + char *token = strtok(value, ","); + cfg->hidden_dirs_count = 0; + while (token && cfg->hidden_dirs_count < 10) { + while (*token == ' ') token++; + strncpy(cfg->hidden_dirs[cfg->hidden_dirs_count], token, 255); + cfg->hidden_dirs[cfg->hidden_dirs_count][255] = '\0'; + cfg->hidden_dirs_count++; + token = strtok(NULL, ","); + } + } + else if (strcmp(key, "files") == 0) { + char *token = strtok(value, ","); + cfg->hidden_files_count = 0; + while (token && cfg->hidden_files_count < 10) { + while (*token == ' ') token++; + strncpy(cfg->hidden_files[cfg->hidden_files_count], token, 255); + cfg->hidden_files[cfg->hidden_files_count][255] = '\0'; + cfg->hidden_files_count++; + token = strtok(NULL, ","); + } + } + } + else if (strcmp(current_section, "preview") == 0) { + if (strcmp(key, "image") == 0) strncpy(cfg->preview_image, value, sizeof(cfg->preview_image) - 1); + else if (strcmp(key, "text") == 0) strncpy(cfg->preview_text, value, sizeof(cfg->preview_text) - 1); + else if (strcmp(key, "audio") == 0) strncpy(cfg->preview_audio, value, sizeof(cfg->preview_audio) - 1); + else if (strcmp(key, "video") == 0) strncpy(cfg->preview_video, value, sizeof(cfg->preview_video) - 1); + else if (strcmp(key, "pdf") == 0) strncpy(cfg->preview_pdf, value, sizeof(cfg->preview_pdf) - 1); + } + } + + fclose(f); + return 0; +} + +void set_default_config(config_t *cfg) { + strcpy(cfg->base_path, "/home/romkazvo/www"); + strcpy(cfg->template_path, "/home/romkazvo/www/cgi-bin/template.html"); + strcpy(cfg->passwd_file, "/home/romkazvo/www/cgi-bin/.htpasswd"); + cfg->max_entries = 1000; + cfg->hidden_dirs_count = 0; + cfg->hidden_files_count = 0; + strcpy(cfg->preview_image, "jpg,jpeg,png,gif,webp,bmp,svg,ico"); + strcpy(cfg->preview_text, "txt,md,html,htm,css,js,json,xml,csv"); + strcpy(cfg->preview_audio, "mp3,wav,ogg,flac,m4a,aac"); + strcpy(cfg->preview_video, "mp4,webm,ogv,mov,avi,mkv"); + strcpy(cfg->preview_pdf, "pdf"); +} + +int is_string_in_list(const char *str, const char *list) { + if (!list || !*list) return 0; + char temp[512]; + strncpy(temp, list, sizeof(temp) - 1); + temp[sizeof(temp) - 1] = '\0'; + char *token = strtok(temp, ","); + while (token) { + while (*token == ' ') token++; + if (strcasecmp(token, str) == 0) return 1; + token = strtok(NULL, ","); + } + return 0; +} + +// === ОСТАЛЬНОЙ КОД (без изменений, только теперь используем g_config вместо #define) === typedef struct { char name[256]; @@ -22,9 +157,11 @@ typedef struct { char encoded_path[1024]; char file_url[1024]; int is_protected; + time_t mtime; + int file_count; + long long dir_size; } entry_t; -// Быстрый буферизированный вывод typedef struct { char *data; size_t size; @@ -32,7 +169,7 @@ typedef struct { } buffer_t; void buffer_init(buffer_t *buf) { - buf->capacity = OUTPUT_BUFFER_SIZE; + buf->capacity = 65536; buf->data = malloc(buf->capacity); buf->size = 0; } @@ -47,48 +184,54 @@ void buffer_append(buffer_t *buf, const char *str) { buf->size += len; } -void buffer_append_size(buffer_t *buf, const char *str, size_t len) { - 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); } -// Функция проверки пароля для папки +int is_safe_path(const char *path) { + if (!path) return 0; + if (strstr(path, "..")) return 0; + if (strstr(path, "./")) return 0; + if (path[0] == '/') return 0; + if (strstr(path, "//")) return 0; + return 1; +} + +void safe_path_join(char *result, size_t result_size, const char *base, const char *path) { + if (!is_safe_path(path)) { + snprintf(result, result_size, "%s", base); + return; + } + snprintf(result, result_size, "%s/%s", base, path); +} + int check_folder_password(const char *folder_name, const char *password) { - FILE *f = fopen(PASSWD_FILE, "r"); - if (!f) return 1; // Если нет файла с паролями - доступ разрешен + if (!password || !folder_name || password[0] == '\0') return 0; + + FILE *f = fopen(g_config.passwd_file, "r"); + if (!f) return 1; char line[512]; while (fgets(line, sizeof(line), f)) { - // Убираем перевод строки line[strcspn(line, "\r\n")] = 0; - char *colon = strchr(line, ':'); if (!colon) continue; - *colon = '\0'; char *folder = line; char *pass = colon + 1; - if (strcmp(folder, folder_name) == 0) { fclose(f); return strcmp(pass, password) == 0; } } fclose(f); - return 1; // Папка не найдена в списке - доступ разрешен + return 1; } -// Проверяет, защищена ли папка паролем int is_folder_protected(const char *folder_name) { - FILE *f = fopen(PASSWD_FILE, "r"); + if (!folder_name || folder_name[0] == '\0') return 0; + + FILE *f = fopen(g_config.passwd_file, "r"); if (!f) return 0; char line[512]; @@ -96,7 +239,6 @@ int is_folder_protected(const char *folder_name) { line[strcspn(line, "\r\n")] = 0; char *colon = strchr(line, ':'); if (!colon) continue; - *colon = '\0'; if (strcmp(line, folder_name) == 0) { fclose(f); @@ -107,121 +249,106 @@ int is_folder_protected(const char *folder_name) { return 0; } +int count_files_in_dir(const char *path) { + DIR *dir = opendir(path); + if (!dir) return 0; + struct dirent *entry; + int count = 0; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + if (entry->d_name[0] == '.') continue; + int hidden = 0; + 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 (hidden) continue; + count++; + } + closedir(dir); + return count; +} + +long long get_dir_size(const char *path) { + DIR *dir; + struct dirent *entry; + struct stat statbuf; + char fullpath[1024]; + long long size = 0; + dir = opendir(path); + if (!dir) return 0; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); + if (stat(fullpath, &statbuf) == 0) { + if (S_ISDIR(statbuf.st_mode)) size += get_dir_size(fullpath); + else if (S_ISREG(statbuf.st_mode)) size += statbuf.st_size; + } + } + closedir(dir); + return size; +} + const char* get_file_icon(const char* filename) { const char *ext = strrchr(filename, '.'); if (!ext) return "📄"; - ext++; - - if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 || - strcasecmp(ext, "png") == 0 || strcasecmp(ext, "gif") == 0 || - strcasecmp(ext, "webp") == 0 || strcasecmp(ext, "bmp") == 0) - return "🖼️"; - - if (strcasecmp(ext, "mp3") == 0 || strcasecmp(ext, "wav") == 0 || - strcasecmp(ext, "flac") == 0 || strcasecmp(ext, "ogg") == 0) - return "🎵"; - - if (strcasecmp(ext, "mp4") == 0 || strcasecmp(ext, "avi") == 0 || - strcasecmp(ext, "mkv") == 0 || strcasecmp(ext, "mov") == 0) - return "🎬"; - + if (is_string_in_list(ext, g_config.preview_image)) return "🖼️"; + if (is_string_in_list(ext, g_config.preview_audio)) return "🎵"; + if (is_string_in_list(ext, g_config.preview_video)) return "🎬"; if (strcasecmp(ext, "zip") == 0 || strcasecmp(ext, "rar") == 0 || strcasecmp(ext, "7z") == 0 || strcasecmp(ext, "tar") == 0 || - strcasecmp(ext, "gz") == 0) - return "📦"; - + strcasecmp(ext, "gz") == 0) return "📦"; if (strcasecmp(ext, "pdf") == 0) return "📕"; if (strcasecmp(ext, "doc") == 0 || strcasecmp(ext, "docx") == 0) return "📘"; if (strcasecmp(ext, "xls") == 0 || strcasecmp(ext, "xlsx") == 0) return "📗"; if (strcasecmp(ext, "txt") == 0) return "📝"; - return "📄"; } int is_previewable(const char* filename) { const char *ext = strrchr(filename, '.'); if (!ext) return 0; - ext++; - - if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 || - strcasecmp(ext, "png") == 0 || strcasecmp(ext, "gif") == 0 || - strcasecmp(ext, "webp") == 0 || strcasecmp(ext, "bmp") == 0 || - strcasecmp(ext, "svg") == 0 || strcasecmp(ext, "ico") == 0) - return 1; - - if (strcasecmp(ext, "txt") == 0 || strcasecmp(ext, "md") == 0 || - strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0 || - strcasecmp(ext, "css") == 0 || strcasecmp(ext, "js") == 0 || - strcasecmp(ext, "json") == 0 || strcasecmp(ext, "xml") == 0 || - strcasecmp(ext, "csv") == 0) - return 1; - - if (strcasecmp(ext, "pdf") == 0) - return 1; - - if (strcasecmp(ext, "mp3") == 0 || strcasecmp(ext, "wav") == 0 || - strcasecmp(ext, "ogg") == 0 || strcasecmp(ext, "flac") == 0 || - strcasecmp(ext, "m4a") == 0 || strcasecmp(ext, "aac") == 0) - return 1; - - if (strcasecmp(ext, "mp4") == 0 || strcasecmp(ext, "webm") == 0 || - strcasecmp(ext, "ogv") == 0 || strcasecmp(ext, "mov") == 0 || - strcasecmp(ext, "avi") == 0 || strcasecmp(ext, "mkv") == 0) - return 1; - - return 0; + return (is_string_in_list(ext, g_config.preview_image) || + is_string_in_list(ext, g_config.preview_text) || + is_string_in_list(ext, g_config.preview_audio) || + is_string_in_list(ext, g_config.preview_video) || + is_string_in_list(ext, g_config.preview_pdf)); } int compare_entries(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; - return strcasecmp(entryA->name, entryB->name); } -void format_size(long size, char* buffer) { - if (size < 1024) { - snprintf(buffer, 32, "%ld 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_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; + 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++; } @@ -231,65 +358,41 @@ 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) { 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) { - *p++ = c; - decoded_len++; - } else if (c >= 0x20 || c == 0x0A || c == 0x0D) { - *p++ = c; - decoded_len++; - } else { - *p++ = '_'; - decoded_len++; - } + if (c >= 0x80) { *p++ = c; decoded_len++; } + else if (c >= 0x20 || c == 0x0A || c == 0x0D) { *p++ = c; decoded_len++; } + else { *p++ = '_'; decoded_len++; } src += 3; - } else { - *p++ = *src++; - decoded_len++; - } + } else { *p++ = *src++; decoded_len++; } } else if (*src == '+') { - *p++ = ' '; - decoded_len++; - src++; + *p++ = ' '; decoded_len++; src++; } else { - *p++ = *src++; - decoded_len++; + *p++ = *src++; decoded_len++; } } *p = '\0'; } -void safe_path_join(char *result, size_t result_size, const char *base, const char *path) { - snprintf(result, result_size, "%s/%s", base, path); -} - void print_breadcrumb(buffer_t *buf, const char *display_path) { 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 link[4096]; snprintf(link, sizeof(link), "%s", encoded, token); buffer_append(buf, link); - token = strtok(NULL, "/"); } free(path_copy); @@ -315,13 +418,10 @@ void print_password_form(buffer_t *buf, const char *folder_name) { 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, "❌ Неверный пароль! Попробуйте снова."); } - buffer_append(buf, "

\n"); buffer_append(buf, " ← Вернуться на главную\n"); buffer_append(buf, "
\n"); @@ -334,22 +434,22 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat struct stat file_stat; char full_path[1024]; - entry_t entries[MAX_ENTRIES]; + entry_t *entries = malloc(sizeof(entry_t) * g_config.max_entries); + if (!entries) { + buffer_append(buf, "
Ошибка выделения памяти
"); + return; + } + int entry_count = 0; int dir_count = 0, file_count = 0; long long total_size = 0; - // Проверяем, не защищена ли текущая папка паролем char folder_name[256] = ""; if (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); - } + if (last_slash) strcpy(folder_name, last_slash + 1); + else strcpy(folder_name, display_path); - // Проверяем пароль char *query_string = getenv("QUERY_STRING"); char *password = NULL; if (query_string) { @@ -362,7 +462,6 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat char pass_buf[256]; strncpy(pass_buf, pass_start, pass_len); pass_buf[pass_len] = '\0'; - // URL декодируем пароль char decoded_pass[256]; url_decode_enhanced(pass_buf, decoded_pass, sizeof(decoded_pass)); password = decoded_pass; @@ -372,8 +471,8 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat if (is_folder_protected(folder_name)) { if (!password || !check_folder_password(folder_name, password)) { - // Показываем форму ввода пароля print_password_form(buf, folder_name); + free(entries); return; } } @@ -382,14 +481,12 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat dir = opendir(base_path); if (!dir) { char alt_path[1024]; - snprintf(alt_path, sizeof(alt_path), "/home/romkazvo/www"); + 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)); + 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"); @@ -402,27 +499,25 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat buffer_append(buf, error_msg); buffer_append(buf, "

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

\n"); buffer_append(buf, "
\n"); + free(entries); return; } } - // Предварительная обработка записей - while ((entry = readdir(dir)) != NULL && entry_count < MAX_ENTRIES) { - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) - continue; - if (strcmp(entry->d_name, "cgi-bin") == 0) - continue; - // Скрываем файл с паролями (на всякий случай, если кто-то сможет получить доступ) - if (strcmp(entry->d_name, ".htpasswd") == 0) - continue; - if (strcmp(entry->d_name, ".htaccess") == 0) - continue; - // Скрываем все файлы, начинающиеся с точки - if (entry->d_name[0] == '.') - continue; + 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) { strncpy(entries[entry_count].name, entry->d_name, sizeof(entries[0].name) - 1); entries[entry_count].name[sizeof(entries[0].name) - 1] = '\0'; @@ -430,8 +525,11 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat 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; @@ -441,22 +539,20 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat } char new_path[2048]; - snprintf(new_path, sizeof(new_path), "%s%s%s", display_path, - display_path[0] ? "/" : "", entries[entry_count].name); + 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; - - snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), - "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); - - file_count++; + snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); total_size += file_stat.st_size; + file_count++; } else { continue; } @@ -471,14 +567,13 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat buffer_append(buf, "

Здесь пусто

\n"); buffer_append(buf, "

В этой директории нет файлов или папок

\n"); buffer_append(buf, "\n"); + free(entries); return; } qsort(entries, entry_count, sizeof(entry_t), compare_entries); - buffer_append(buf, "\n"); - // Статистика char total_size_str[32]; format_size(total_size, total_size_str); char stats[256]; @@ -582,10 +688,12 @@ void print_content(buffer_t *buf, const char *base_path, const char *display_pat "
\n Папки: %d | Файлы: %d | Общий размер: %s\n
\n", dir_count, file_count, total_size_str); buffer_append(buf, stats); + + free(entries); } void print_template(const char *base_path, const char *display_path) { - FILE *file = fopen(TEMPLATE_PATH, "r"); + FILE *file = fopen(g_config.template_path, "r"); if (!file) { printf("Error: Cannot read template\n"); return; @@ -614,9 +722,16 @@ int main() { setlocale(LC_ALL, "en_US.UTF-8"); setlocale(LC_CTYPE, "en_US.UTF-8"); + // Загружаем конфиг + if (load_config("/home/romkazvo/www/cgi-bin/config.ini", &g_config) != 0) { + // Если конфиг не найден — используем дефолты + set_default_config(&g_config); + } + printf("Content-type: text/html; charset=utf-8\n\n"); - char base_path[1024] = "/home/romkazvo/www"; + char base_path[1024]; + strcpy(base_path, g_config.base_path); char display_path[1024] = ""; char safe_display_path[1024] = ""; @@ -627,35 +742,30 @@ int main() { path_start += 5; char *path_end = strchr(path_start, '&'); int path_len = path_end ? path_end - path_start : strlen(path_start); - if (path_len > 0 && path_len < sizeof(display_path) - 1) { char encoded_path[1024]; strncpy(encoded_path, path_start, path_len); encoded_path[path_len] = '\0'; - url_decode_enhanced(encoded_path, display_path, sizeof(display_path)); - - strncpy(safe_display_path, display_path, sizeof(safe_display_path) - 1); - safe_display_path[sizeof(safe_display_path) - 1] = '\0'; - - safe_path_join(base_path, sizeof(base_path), "/home/romkazvo/www", safe_display_path); + if (!is_safe_path(display_path)) { + display_path[0] = '\0'; + strcpy(base_path, g_config.base_path); + } else { + strncpy(safe_display_path, display_path, sizeof(safe_display_path) - 1); + safe_display_path[sizeof(safe_display_path) - 1] = '\0'; + safe_path_join(base_path, sizeof(base_path), g_config.base_path, safe_display_path); + } } } - // Обработка неправильного пароля 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 (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) { @@ -668,14 +778,23 @@ int main() { pass_buf[pass_len] = '\0'; char decoded_pass[256]; url_decode_enhanced(pass_buf, decoded_pass, sizeof(decoded_pass)); - - if (!check_folder_password(folder_name, decoded_pass)) { - // Неверный пароль - редирект с error=1 + if (decoded_pass[0] == '\0') { char encoded_path[1024]; url_encode(display_path, encoded_path, sizeof(encoded_path)); printf("Location: /cgi-bin/index.cgi?path=%s&error=1\n\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("Location: /cgi-bin/index.cgi?path=%s&error=1\n\n", encoded_path); + return 0; + } + } else { + char encoded_path[1024]; + url_encode(display_path, encoded_path, sizeof(encoded_path)); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\n\n", encoded_path); + return 0; } } } @@ -686,4 +805,4 @@ int main() { print_template(base_path, display_path); return 0; -} \ No newline at end of file +} diff --git a/style.css b/style.css index f1559be..0959e48 100755 --- a/style.css +++ b/style.css @@ -1,715 +1,725 @@ -: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); -} - -.dark-theme { - --bg-primary: #121212; - --bg-secondary: #1e1e1e; - --bg-header: linear-gradient(135deg, #4a5568 0%, #2d3748 100%); - --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); -} - -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; -} - -body.loaded * { - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; -} - -.container { - max-width: 100%; - margin: 0 auto; - background: var(--bg-secondary); - min-width: 320px; - min-height: 100vh; - box-shadow: var(--shadow); -} - -@media (min-width: 768px) { - .container { - max-width: 1000px; - margin: 10px auto; - min-height: auto; - border-radius: 8px; - overflow: hidden; - } -} - -.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); - 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; - position: relative; -} - -h1 { - font-size: 1.8rem; - font-weight: 700; - margin-bottom: 0.5rem; -} - -.content { - padding: 1rem; - min-width: 0; -} - -.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; - 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; - align-items: center; - flex-wrap: wrap; -} - -li:last-child { border-bottom: none; } - -li:hover { - background-color: var(--hover-bg); -} - -/* Стили для строки файла */ -.file-row { - display: flex; - align-items: center; - justify-content: space-between; - flex-grow: 1; - min-width: 0; - gap: 1rem; -} - -.file-link { - color: var(--accent-blue); - font-weight: 500; - text-decoration: none; - display: flex; - align-items: center; - flex-grow: 1; - min-width: 0; -} - -.dir-link { - color: var(--accent-green); - font-weight: 600; - text-decoration: none; - display: flex; - align-items: center; - flex-grow: 1; -} - -.file-controls { - display: flex; - align-items: center; - gap: 0.75rem; - flex-shrink: 0; -} - -.size { - color: var(--text-secondary); - font-size: 0.85rem; - white-space: nowrap; - min-width: 70px; - text-align: right; -} - -.file-icon { - margin-right: 1rem; - font-size: 1.4rem; - 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 { - background: transparent; - color: var(--text-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; - width: 32px; - height: 32px; - cursor: pointer; - font-size: 0.85rem; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - opacity: 0.7; -} - -.preview-btn:hover { - background: var(--accent-blue); - color: white; - border-color: var(--accent-blue); - opacity: 1; - transform: scale(1.05); -} - -/* Модальное окно предпросмотра */ -.preview-modal { - display: none; - position: fixed; - z-index: 2000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.8); - backdrop-filter: blur(5px); - overflow: auto; -} - -.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); - display: flex; - flex-direction: column; - overflow: hidden; - transition: all 0.3s ease; -} - -.preview-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - border-bottom: 1px solid var(--border-color); - background: var(--bg-primary); - flex-shrink: 0; -} - -.preview-title { - font-weight: 600; - color: var(--text-primary); - margin: 0; - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-right: 1rem; -} - -.close-preview { - background: #dc3545; - color: white; - border: none; - border-radius: 4px; - width: 36px; - height: 36px; - cursor: pointer; - font-size: 1.2rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s ease; -} - -.close-preview:hover { - background: #c82333; -} - -.preview-iframe { - flex-grow: 1; - border: none; - background: white; - min-height: 0; -} - -.preview-iframe.pdf { - background: var(--bg-primary); -} - -/* Стили для контейнера изображения */ -.preview-image-container { - display: flex; - align-items: center; - justify-content: center; - flex-grow: 1; - padding: 10px; - 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: 8px; - box-shadow: 0 4px 20px rgba(0,0,0,0.2); - display: block; -} - -/* Улучшаем скроллинг для очень больших изображений */ -.preview-modal::-webkit-scrollbar { - width: 8px; -} - -.preview-modal::-webkit-scrollbar-track { - background: rgba(255,255,255,0.1); -} - -.preview-modal::-webkit-scrollbar-thumb { - background: rgba(255,255,255,0.3); - border-radius: 4px; -} - -.preview-modal::-webkit-scrollbar-thumb:hover { - background: rgba(255,255,255,0.5); -} - -/* Адаптивность */ -@media (max-width: 768px) { - .preview-content { - margin: 2.5% auto; - } - - .preview-header { - padding: 0.75rem; - } - - .preview-title { - font-size: 0.9rem; - } - - .preview-image-container { - padding: 5px; - } -} - -@media (max-width: 480px) { - .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.75rem; - } - .breadcrumb { - padding: 0.75rem; - font-size: 0.85rem; - } - li { - padding: 0.75rem 0.25rem; - } - .file-row { - gap: 0.5rem; - } - .file-controls { - gap: 0.5rem; - } - .size { - font-size: 0.8rem; - min-width: 60px; - } - .file-icon { - margin-right: 0.75rem; - font-size: 1.2rem; - width: 20px; - } - .file-name { - font-size: 0.9rem; - } - .section-title { - font-size: 0.85rem; - padding: 0.5rem; - margin: 1rem 0 0.25rem 0; - } - .footer { - padding: 0.75rem; - font-size: 0.85rem; - } - .preview-btn { - width: 28px; - height: 28px; - 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); - } -} - -@media (max-width: 360px) { - .container { - min-width: 100%; - margin: 0; - } - .content { - padding: 0.5rem; - } - .file-name { - font-size: 0.85rem; - } - .size { - font-size: 0.75rem; - } - .file-row { - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - } - .file-controls { - align-self: flex-end; - } - .file-link { - width: 100%; - } -} - -/* Стили для формы ввода пароля */ -.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; -} - -.protected-folder .lock-icon { - font-size: 0.8rem; - margin-left: 0.5rem; - opacity: 0.7; -} - -/* Адаптивность для формы пароля */ -@media (max-width: 480px) { - .password-form { - padding: 1.5rem; - margin: 0 0.5rem; - } - - .password-form h2 { - font-size: 1.1rem; - } - - .password-form input[type="password"], - .password-form button { - font-size: 0.95rem; - padding: 0.6rem; - } -} +: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); +} + +.dark-theme { + --bg-primary: #121212; + --bg-secondary: #1e1e1e; + --bg-header: linear-gradient(135deg, #4a5568 0%, #2d3748 100%); + --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); +} + +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; +} + +body.loaded * { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +.container { + max-width: 100%; + margin: 0 auto; + background: var(--bg-secondary); + min-width: 320px; + min-height: 100vh; + box-shadow: var(--shadow); +} + +@media (min-width: 768px) { + .container { + max-width: 1000px; + margin: 10px auto; + min-height: auto; + border-radius: 8px; + overflow: hidden; + } +} + +.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); + 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; + position: relative; +} + +h1 { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.content { + padding: 1rem; + min-width: 0; +} + +.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; + 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: 0.75rem 0.5rem; + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + flex-wrap: nowrap; /* Исправлено: теперь не переносятся */ +} + +li:last-child { border-bottom: none; } + +li:hover { + background-color: var(--hover-bg); +} + +/* Стили для строки файла */ +.file-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-grow: 1; + min-width: 0; + gap: 0.75rem; + flex-wrap: nowrap; /* Запрещаем перенос */ +} + +.file-link { + color: var(--accent-blue); + font-weight: 500; + text-decoration: none; + display: flex; + align-items: center; + flex-grow: 1; + min-width: 0; +} + +.dir-link { + color: var(--accent-green); + font-weight: 600; + text-decoration: none; + display: flex; + align-items: center; + flex-grow: 1; +} + +.file-controls { + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +.file-meta { + color: var(--text-muted); + font-size: 0.85rem; + white-space: nowrap; + text-align: right; + font-variant-numeric: tabular-nums; +} + +.file-meta.date { + min-width: 90px; +} + +.file-meta.size { + min-width: 70px; +} + +.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 { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + width: 32px; + height: 32px; + cursor: pointer; + font-size: 0.85rem; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + opacity: 0.7; + flex-shrink: 0; +} + +.preview-btn:hover, .copy-btn:hover { + background: var(--accent-blue); + color: white; + border-color: var(--accent-blue); + opacity: 1; + transform: scale(1.05); +} + +/* Стили для формы ввода пароля */ +.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.8); + backdrop-filter: blur(5px); + overflow: auto; +} + +.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); + display: flex; + flex-direction: column; + overflow: hidden; + transition: all 0.3s ease; +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); + flex-shrink: 0; +} + +.preview-title { + font-weight: 600; + color: var(--text-primary); + margin: 0; + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 1rem; +} + +.close-preview { + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + width: 36px; + height: 36px; + cursor: pointer; + font-size: 1.2rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; +} + +.close-preview:hover { + background: #c82333; +} + +.preview-iframe { + flex-grow: 1; + border: none; + background: white; + min-height: 0; +} + +.preview-iframe.pdf { + background: var(--bg-primary); +} + +.preview-image-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + padding: 10px; + 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: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.2); + display: block; +} + +.preview-modal::-webkit-scrollbar { + width: 8px; +} + +.preview-modal::-webkit-scrollbar-track { + background: rgba(255,255,255,0.1); +} + +.preview-modal::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.3); + border-radius: 4px; +} + +.preview-modal::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.5); +} + +/* Уведомление */ +#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) { + .file-meta { + font-size: 0.75rem; + } + .file-meta.date { + min-width: 75px; + } + .file-meta.size { + min-width: 60px; + } + .preview-content { + margin: 2.5% auto; + } + .preview-header { + padding: 0.75rem; + } + .preview-title { + font-size: 0.9rem; + } + .preview-image-container { + padding: 5px; + } +} + +@media (max-width: 480px) { + /* Скрываем дату на телефонах */ + .file-meta.date { + display: none !important; + } + /* Уменьшаем размер кнопок */ + .preview-btn, .copy-btn { + width: 28px; + height: 28px; + font-size: 0.75rem; + } + .file-meta.size { + font-size: 0.7rem; + min-width: 50px; + } + .file-name { + font-size: 0.85rem; + } + .file-icon { + font-size: 1.1rem; + width: 20px; + margin-right: 0.5rem; + } + .file-controls { + gap: 0.3rem; + } + li { + padding: 0.5rem 0.25rem; + } + .file-row { + gap: 0.4rem; + } + .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; + font-size: 0.85rem; + } +} + +@media (max-width: 360px) { + .container { + min-width: 100%; + margin: 0; + } + .file-name { + font-size: 0.8rem; + } + .file-meta.size { + font-size: 0.65rem; + min-width: 40px; + } + .preview-btn, .copy-btn { + width: 24px; + height: 24px; + font-size: 0.7rem; + } +} diff --git a/template.html b/template.html index a99a0bc..69c8db5 100644 --- a/template.html +++ b/template.html @@ -23,11 +23,6 @@ + @@ -101,25 +144,25 @@ const content = document.getElementById('previewContent'); const title = document.getElementById('previewTitle'); const frame = document.getElementById('previewFrame'); - + if (!modal || !content || !title || !frame) { console.error('Modal elements not found!'); return; } - + frame.style.display = 'none'; - + title.textContent = '🔒 Введите пароль для папки: ' + folderName; - + const oldContainer = document.getElementById('passwordFormContainer'); if (oldContainer) { oldContainer.remove(); } - + const container = document.createElement('div'); container.id = 'passwordFormContainer'; container.style.cssText = 'display:flex;justify-content:center;align-items:center;padding:2rem;flex-grow:1;min-height:300px;'; - + container.innerHTML = `
🔒
@@ -136,9 +179,9 @@
`; - + content.appendChild(container); - + content.style.width = '500px'; content.style.height = 'auto'; content.style.maxWidth = '90vw'; @@ -148,10 +191,10 @@ content.style.left = '50%'; content.style.top = '50%'; content.style.transform = 'translate(-50%, -50%)'; - + modal.style.display = 'block'; document.body.style.overflow = 'hidden'; - + setTimeout(() => { const input = container.querySelector('input[type="password"]'); if (input) input.focus(); @@ -170,6 +213,51 @@ return true; }; + // ========== НОВАЯ ФУНКЦИЯ КОПИРОВАНИЯ ========== + window.copyLink = function(path) { + const url = window.location.origin + path; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(url).then(function() { + showNotification('✅ Ссылка скопирована!'); + }).catch(function() { + fallbackCopy(url); + }); + } else { + fallbackCopy(url); + } + }; + + function fallbackCopy(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + textarea.style.top = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + showNotification('✅ Ссылка скопирована!'); + } catch (e) { + alert('Не удалось скопировать ссылку'); + } + document.body.removeChild(textarea); + } + + function showNotification(msg) { + const old = document.getElementById('copyNotification'); + if (old) old.remove(); + const div = document.createElement('div'); + div.id = 'copyNotification'; + div.textContent = msg; + document.body.appendChild(div); + setTimeout(() => { + div.style.opacity = '0'; + setTimeout(() => div.remove(), 400); + }, 2500); + } + // ============================================== + window.scrollToTop = function() { window.scrollTo({ top: 0, @@ -189,13 +277,13 @@ window.toggleTheme = function() { const isDark = document.body.classList.contains('dark-theme'); const newTheme = isDark ? 'light' : 'dark'; - + document.body.classList.toggle('dark-theme'); document.documentElement.classList.toggle('dark-theme'); - + const button = document.querySelector('.theme-toggle'); button.textContent = isDark ? '🌙' : '☀️'; - + const date = new Date(); date.setFullYear(date.getFullYear() + 1); document.cookie = 'theme=' + newTheme + '; expires=' + date.toUTCString() + '; path=/'; @@ -206,26 +294,26 @@ const frame = document.getElementById('previewFrame'); const content = document.getElementById('previewContent'); const title = document.getElementById('previewTitle'); - + const oldContainer = document.getElementById('passwordFormContainer'); if (oldContainer) { oldContainer.remove(); } - + frame.style.display = 'block'; - + const fileName = fileUrl.split('/').pop(); title.textContent = 'Предпросмотр: ' + fileName; - + const fileExt = fileName.split('.').pop().toLowerCase(); const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico']; const textTypes = ['txt', 'md', 'html', 'htm', 'css', 'js', 'json', 'xml', 'csv']; const audioTypes = ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac']; const videoTypes = ['mp4', 'webm', 'ogv', 'mov', 'avi', 'mkv']; const isPDF = fileExt === 'pdf'; - + frame.src = ''; - + let imgContainer = document.getElementById('previewImageContainer'); if (!imgContainer) { imgContainer = document.createElement('div'); @@ -235,29 +323,29 @@ } imgContainer.style.display = 'none'; imgContainer.innerHTML = ''; - + if (imageTypes.includes(fileExt)) { frame.style.display = 'none'; imgContainer.style.display = 'flex'; - + const img = document.createElement('img'); img.src = fileUrl; img.alt = fileName; img.className = 'preview-image'; - + img.onload = function() { adjustModalForImage(this); }; - + img.onerror = function() { console.error('Failed to load image:', fileUrl); frame.style.display = 'block'; imgContainer.style.display = 'none'; frame.src = fileUrl; }; - + imgContainer.appendChild(img); - + content.style.width = '90vw'; content.style.height = '90vh'; content.style.maxWidth = 'none'; @@ -267,11 +355,11 @@ content.style.left = 'auto'; content.style.top = 'auto'; content.style.transform = 'none'; - + } else { frame.style.display = 'block'; imgContainer.style.display = 'none'; - + if (textTypes.includes(fileExt)) { content.style.width = '80vw'; content.style.height = '70vh'; @@ -299,16 +387,16 @@ content.style.maxWidth = '1000px'; content.style.maxHeight = '800px'; } - + content.style.margin = '2% auto'; content.style.position = 'relative'; content.style.left = 'auto'; content.style.top = 'auto'; content.style.transform = 'none'; - + frame.src = fileUrl; } - + modal.style.display = 'block'; document.body.style.overflow = 'hidden'; }; @@ -317,22 +405,22 @@ const content = document.getElementById('previewContent'); const maxWidth = window.innerWidth * 0.95; const maxHeight = window.innerHeight * 0.95; - + const imgWidth = img.naturalWidth; const imgHeight = img.naturalHeight; - + let scale = 1; - + if (imgWidth > maxWidth) { scale = Math.min(scale, maxWidth / imgWidth); } if (imgHeight > maxHeight) { scale = Math.min(scale, maxHeight / imgHeight); } - + const displayWidth = imgWidth * scale; const displayHeight = imgHeight * scale; - + content.style.width = (displayWidth + 20) + 'px'; content.style.height = (displayHeight + 80) + 'px'; content.style.margin = 'auto'; @@ -346,17 +434,17 @@ const modal = document.getElementById('previewModal'); const frame = document.getElementById('previewFrame'); const content = document.getElementById('previewContent'); - + modal.style.display = 'none'; frame.src = ''; frame.classList.remove('pdf'); frame.style.display = 'block'; - + const oldContainer = document.getElementById('passwordFormContainer'); if (oldContainer) { oldContainer.remove(); } - + content.style.width = ''; content.style.height = ''; content.style.maxWidth = ''; @@ -366,16 +454,16 @@ content.style.left = ''; content.style.top = ''; content.style.transform = ''; - + const imgContainer = document.getElementById('previewImageContainer'); if (imgContainer) { imgContainer.style.display = 'none'; imgContainer.innerHTML = ''; } - + const title = document.getElementById('previewTitle'); title.textContent = 'Предпросмотр файла'; - + document.body.style.overflow = 'auto'; }; @@ -394,7 +482,7 @@ document.addEventListener('DOMContentLoaded', function() { const savedTheme = getCookie('theme'); const button = document.querySelector('.theme-toggle'); - + if (savedTheme === 'dark') { document.body.classList.add('dark-theme'); button.textContent = '☀️';