From e7700a3391374fabbb66bc3cae18575dac986d70 Mon Sep 17 00:00:00 2001 From: Web-serfer Date: Thu, 7 May 2026 16:55:35 +0500 Subject: [PATCH] =?UTF-8?q?opt:=20=D1=83=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D1=87=D0=B5=D1=82=D1=87=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20-=20=D0=BE=D0=B4=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20=D0=B2=20=D0=91=D0=94=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D1=82=D1=8B=D1=81=D1=8F=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - site_stats: 1 запись с полями today/total вместо site_visitors - posts: today_views + last_reset вместо post_views коллекции - Удалены старые коллекции site_visitors и post_views --- .../layout/footer/VisitorCounter.astro | 18 +- frontend/src/layouts/ArticleLayout.astro | 7 +- frontend/src/pages/api/increment-views.ts | 102 ++----- frontend/src/pages/api/visitors.ts | 131 +++------ scripts/sync-posts-from-server.ts | 271 ------------------ scripts/sync-posts.ts | 158 ---------- 6 files changed, 65 insertions(+), 622 deletions(-) delete mode 100644 scripts/sync-posts-from-server.ts delete mode 100644 scripts/sync-posts.ts diff --git a/frontend/src/components/layout/footer/VisitorCounter.astro b/frontend/src/components/layout/footer/VisitorCounter.astro index 77b9658..59e7e51 100644 --- a/frontend/src/components/layout/footer/VisitorCounter.astro +++ b/frontend/src/components/layout/footer/VisitorCounter.astro @@ -28,20 +28,10 @@ const { today = 0, total = 0 } = Astro.props; const valueEls = root.querySelectorAll('.counter-value'); if (valueEls.length < 2) return; - const todayStr = new Date().toDateString(); - const lastVisit = localStorage.getItem('site_visited'); - - // Если уже посещали сегодня - помечаем как повторный визит - const isRepeat = lastVisit === todayStr; - const apiUrl = isRepeat ? '/api/visitors?repeat=true' : '/api/visitors'; - - fetch(apiUrl).then(r => r.json()).then(d => { - if (typeof d.todayVisitors === 'number') { - if (d.isNewVisitor) { - localStorage.setItem('site_visited', todayStr); - } - if (valueEls[0]) valueEls[0].textContent = d.todayVisitors + ' сегодня'; - if (valueEls[1]) valueEls[1].textContent = d.totalVisitors + ' всего'; + fetch('/api/visitors').then(r => r.json()).then(d => { + if (typeof d.today === 'number') { + if (valueEls[0]) valueEls[0].textContent = d.today + ' сегодня'; + if (valueEls[1]) valueEls[1].textContent = d.total + ' всего'; } }).catch(() => {}); })(); diff --git a/frontend/src/layouts/ArticleLayout.astro b/frontend/src/layouts/ArticleLayout.astro index b8588ba..1702db1 100644 --- a/frontend/src/layouts/ArticleLayout.astro +++ b/frontend/src/layouts/ArticleLayout.astro @@ -320,22 +320,17 @@ const { // 5. СЧЁТЧИК ПРОСМОТРОВ const viewsEl = document.querySelector('.meta-views') as HTMLElement & { dataset: { postId: string } }; - console.log('[Views] Element found:', viewsEl, 'postId:', viewsEl?.dataset?.postId); if (viewsEl?.dataset?.postId) { const postId = viewsEl.dataset.postId; - console.log('[Views] Fetching for post:', postId); fetch(`/api/increment-views?postId=${postId}`) .then(res => res.json()) .then(data => { - console.log('[Views] Response:', data); if (data.views !== undefined) { viewsEl.textContent = formatViews(data.views); } }) - .catch(err => { - console.error('[Views] Error:', err); - }); + .catch(() => {}); } function formatViews(n: number): string { diff --git a/frontend/src/pages/api/increment-views.ts b/frontend/src/pages/api/increment-views.ts index 682b414..905b34d 100644 --- a/frontend/src/pages/api/increment-views.ts +++ b/frontend/src/pages/api/increment-views.ts @@ -1,44 +1,8 @@ import type { APIRoute } from 'astro'; -import crypto from 'crypto'; const PB_POCKETBASE_URL = import.meta.env.PB_POCKETBASE_URL || 'http://127.0.0.1:8090'; - const POCKETBASE_ID_REGEX = /^[a-z0-9]{15}$/; -function getClientIp(request: Request): string { - const forwarded = request.headers.get('x-forwarded-for'); - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - const realIp = request.headers.get('x-real-ip'); - if (realIp) { - return realIp; - } - // Для localhost использовать заголовок Host или default - const host = request.headers.get('host') || 'localhost'; - return host.includes('localhost') || host.includes('127.0.0.1') ? 'localhost' : 'unknown'; -} - -function generateVisitorHash(ip: string, userAgent: string): string { - return crypto.createHash('sha256').update(ip + userAgent).digest('hex').slice(0, 32); -} - -async function pbRequest(method: string, path: string, body?: object) { - const url = `${PB_POCKETBASE_URL}${path}`; - const options: RequestInit = { - method, - headers: { 'Content-Type': 'application/json' }, - }; - if (body) options.body = JSON.stringify(body); - - const res = await fetch(url, options); - if (!res.ok) { - const err = await res.text(); - throw new Error(`PB ${method} ${path}: ${res.status} - ${err}`); - } - return res.json(); -} - function jsonResponse(data: object, status = 200): Response { return new Response(JSON.stringify(data), { status, @@ -46,7 +10,7 @@ function jsonResponse(data: object, status = 200): Response { }); } -export const GET: APIRoute = async ({ request, url }) => { +export const GET: APIRoute = async ({ url }) => { try { const postId = url.searchParams.get('postId'); @@ -54,59 +18,43 @@ export const GET: APIRoute = async ({ request, url }) => { return jsonResponse({ error: 'Некорректный postId' }, 400); } + // Получаем пост let post; try { - post = await pbRequest('GET', `/api/collections/posts/records/${postId}`); + post = await fetch(`${PB_POCKETBASE_URL}/api/collections/posts/records/${postId}`, { + headers: { 'Content-Type': 'application/json' } + }).then(r => r.json()); } catch { return jsonResponse({ error: 'Пост не найден' }, 404); } - const ip = getClientIp(request); - const userAgent = request.headers.get('user-agent') || 'unknown'; - const visitorHash = generateVisitorHash(ip, userAgent); + if (!post || !post.id) { + return jsonResponse({ error: 'Пост не найден' }, 404); + } const now = new Date(); - const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); - const yesterdayStr = yesterday.toISOString(); + const todayStr = now.toISOString().split('T')[0]; - let existingViews; - try { - const res = await pbRequest('GET', `/api/collections/post_views/records?filter=(post="${postId}")&&(visitor_hash="${visitorHash}")&&(created>="${yesterdayStr}")&perPage=1`); - existingViews = { totalItems: res.totalItems || 0 }; - } catch { - existingViews = { totalItems: 0 }; + let { views = 0, today_views = 0, last_reset = '' } = post; + + // Если новый день - сбрасываем today_views + if (last_reset !== todayStr) { + today_views = 0; + last_reset = todayStr; } - let isNewView = false; + // Инкремент + views++; + today_views++; - if (existingViews.totalItems === 0) { - isNewView = true; + // Обновляем пост + await fetch(`${PB_POCKETBASE_URL}/api/collections/posts/records/${postId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ views, today_views, last_reset }) + }); - try { - await pbRequest('POST', '/api/collections/post_views/records', { - post: postId, - visitor_hash: visitorHash, - ip: ip, - user_agent: userAgent, - }); - } catch { - // ignore - } - } - - let totalViews = post.views || 0; - - if (isNewView) { - totalViews += 1; - - try { - await pbRequest('PATCH', `/api/collections/posts/records/${postId}`, { views: totalViews }); - } catch { - // ignore - } - } - - return jsonResponse({ views: totalViews, isNewView }, 200); + return jsonResponse({ views, today_views }, 200); } catch (error) { console.error('[Increment Views] Error:', error); diff --git a/frontend/src/pages/api/visitors.ts b/frontend/src/pages/api/visitors.ts index 850cc40..20b2f26 100644 --- a/frontend/src/pages/api/visitors.ts +++ b/frontend/src/pages/api/visitors.ts @@ -1,49 +1,7 @@ import type { APIRoute } from 'astro'; -import crypto from 'crypto'; const PB_POCKETBASE_URL = import.meta.env.PB_POCKETBASE_URL || 'http://127.0.0.1:8090'; -function getClientIp(request: Request): string { - const forwarded = request.headers.get('x-forwarded-for'); - if (forwarded) { - return forwarded.split(',')[0].trim(); - } - const realIp = request.headers.get('x-real-ip'); - if (realIp) { - return realIp; - } - const host = request.headers.get('host') || 'localhost'; - return host.includes('localhost') || host.includes('127.0.0.1') ? 'localhost' : 'unknown'; -} - -function generateVisitorHash(ip: string, userAgent: string): string { - // Упрощаем - используем только IP (без порта) для стабильности - const stableIP = ip.split(':').pop() || ip; - // Используем только первые 2 слова из userAgent (браузер + платформа) - const uaParts = userAgent.split(' '); - const stableUA = uaParts.slice(0, 2).join(' '); - return crypto.createHash('sha256').update(stableIP + stableUA).digest('hex').slice(0, 32); -} - -async function pbRequest(method: string, path: string, body?: object) { - const url = `${PB_POCKETBASE_URL}${path}`; - const options: RequestInit = { - method, - headers: { - 'Content-Type': 'application/json', - 'Origin': 'http://127.0.0.1:4321', - }, - }; - if (body) options.body = JSON.stringify(body); - - const res = await fetch(url, options); - if (!res.ok) { - const err = await res.text(); - throw new Error(`PB ${method} ${path}: ${res.status} - ${err}`); - } - return res.json(); -} - function jsonResponse(data: object, status = 200): Response { return new Response(JSON.stringify(data), { status, @@ -53,73 +11,54 @@ function jsonResponse(data: object, status = 200): Response { export const GET: APIRoute = async ({ request }) => { try { - const url = new URL(request.url); - const isRepeatVisit = url.searchParams.get('repeat') === 'true'; - - const ip = getClientIp(request); - const userAgent = request.headers.get('user-agent') || 'unknown'; - const visitorHash = generateVisitorHash(ip, userAgent); - const now = new Date(); - // Начало текущего дня по UTC (00:00 UTC) - const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0)); - // PocketBase использует формат YYYY-MM-DD HH:MM:SS - const todayStartStr = todayStart.toISOString().replace('T', ' ').replace('Z', ''); + const todayStr = now.toISOString().split('T')[0]; - // Проверяем был ли посетитель сегодня (с 00:00 UTC) - const filterTodayVisitor = `visitor_hash="${visitorHash}" && created >= "${todayStartStr}"`; + // Получаем текущую запись stats + const res = await fetch(`${PB_POCKETBASE_URL}/api/collections/site_stats/records?perPage=1`, { + headers: { 'Content-Type': 'application/json' } + }); - let existingVisitor; - try { - const res = await pbRequest('GET', `/api/collections/site_visitors/records?filter=${encodeURIComponent(filterTodayVisitor)}&perPage=1`); - existingVisitor = { totalItems: res.totalItems || 0 }; - } catch { - existingVisitor = { totalItems: 0 }; + if (!res.ok) { + return jsonResponse({ today: 0, total: 0 }, 200); } - let isNewVisitor = false; + const data = await res.json(); + const stats = data.items?.[0]; - // Если это повторный визит по данным клиента - не создаём запись - if (isRepeatVisit) { - isNewVisitor = false; - } - // Создаём запись только если НЕ было посещения сегодня - else if (existingVisitor.totalItems === 0) { - isNewVisitor = true; - try { - await pbRequest('POST', '/api/collections/site_visitors/records', { - visitor_hash: visitorHash, - ip: ip, - user_agent: userAgent, - }); - } catch { - // ignore - } + if (!stats) { + // Нет записи - создаём первую + await fetch(`${PB_POCKETBASE_URL}/api/collections/site_stats/records`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ today: 1, total: 1, last_reset: todayStr }) + }); + return jsonResponse({ today: 1, total: 1 }, 200); } - // Получаем статистику за сегодня (с 00:00 UTC) - const filterToday = `created >= "${todayStartStr}"`; - let todayCount = 0; - let totalCount = 0; - - try { - const resToday = await pbRequest('GET', `/api/collections/site_visitors/records?filter=${encodeURIComponent(filterToday)}&perPage=1`); - todayCount = resToday.totalItems || 0; - } catch { - todayCount = 0; + let { today = 0, total = 0, last_reset = '' } = stats; + + // Если новый день - сбрасываем today + if (last_reset !== todayStr) { + today = 0; + last_reset = todayStr; } - try { - const resTotal = await pbRequest('GET', '/api/collections/site_visitors/records?perPage=1'); - totalCount = resTotal.totalItems || 0; - } catch { - totalCount = 0; - } + // Инкремент + today++; + total++; - return jsonResponse({ todayVisitors: todayCount, totalVisitors: totalCount, isNewVisitor }, 200); + // Обновляем в БД + await fetch(`${PB_POCKETBASE_URL}/api/collections/site_stats/records/${stats.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ today, total, last_reset }) + }); + + return jsonResponse({ today, total }, 200); } catch (error) { console.error('[Visitors] Error:', error); - return jsonResponse({ error: 'Внутренняя ошибка сервера' }, 500); + return jsonResponse({ today: 0, total: 0 }, 200); } }; \ No newline at end of file diff --git a/scripts/sync-posts-from-server.ts b/scripts/sync-posts-from-server.ts deleted file mode 100644 index eb6dc06..0000000 --- a/scripts/sync-posts-from-server.ts +++ /dev/null @@ -1,271 +0,0 @@ -import PocketBase from 'pocketbase'; -import fs from 'fs'; -import path from 'path'; -import https from 'https'; -import http from 'http'; - -const SERVER_URL = 'https://avt-back.ru/'; -const LOCAL_URL = 'http://127.0.0.1:8090'; -const LOCAL_STORAGE_PATH = 'D:/Verstka/production/astro_avtourist/backend/pb_data/storage/pbc_1125843985'; - -const ADMIN_EMAIL = 'redibedi2019@gmail.com'; -const ADMIN_PASSWORD = 'Stalin4444'; - -const localPb = new PocketBase(LOCAL_URL); - -interface Post { - id: string; - title: string; - slug: string; - description: string; - content: string; - author: string; - category: string; - categoryColor: string; - date: string; - readmeTime: string; - image: string; - draft: boolean; - views: number; -} - -const COLLECTION_ID = 'pbc_1125843985'; -const TEMP_DIR = path.join(process.cwd(), 'temp_images'); - -function ensureDir(dir: string) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } -} - -function copyFileSync(src: string, dest: string): boolean { - try { - if (!fs.existsSync(src)) { - return false; - } - ensureDir(path.dirname(dest)); - fs.copyFileSync(src, dest); - return true; - } catch (e) { - return false; - } -} - -async function downloadFile(url: string, destPath: string, token?: string): Promise { - return new Promise((resolve) => { - const protocol = url.startsWith('https') ? https : http; - - const options: https.RequestOptions = {}; - if (token) { - options.headers = { 'Authorization': `Bearer ${token}` }; - } - - console.log(` Headers:`, JSON.stringify(options.headers)); - - const file = fs.createWriteStream(destPath); - - protocol.get(url, (response) => { - if (response.statusCode === 404) { - fs.unlinkSync(destPath); - resolve(false); - return; - } - - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(true); - }); - }).on('error', (err) => { - fs.unlinkSync(destPath); - resolve(false); - }); - }); -} - -async function uploadImage(localPb: PocketBase, filePath: string): Promise { - try { - const formData = new FormData(); - const fileBuffer = fs.readFileSync(filePath); - const fileName = path.basename(filePath); - - const blob = new Blob([fileBuffer]); - const file = new File([blob], fileName); - formData.append('image', file); - - const record = await localPb.collection('posts').create(formData); - return record.image; - } catch (e) { - console.error('Ошибка загрузки изображения:', e); - return null; - } -} - -async function syncPosts() { - console.log('🔄 Синхронизация постов с сервера на локальную БД...\n'); - - ensureDir(TEMP_DIR); - - const adminData = await localPb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); - console.log('✅ Подключено к локальной БД\n'); - - const serverPb = new PocketBase(SERVER_URL); - const authData = await serverPb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); - console.log('✅ Подключено к серверу'); - - const token = authData.token; - - console.log('📥 Получение постов с сервера...'); - const response = await fetch(`${SERVER_URL}api/collections/posts/records?perPage=500`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - - const data = await response.json(); - const serverPosts = data.items as Post[]; - - // Дебаг: смотрю структуру image - if (serverPosts.length > 0) { - console.log('\n📋 Дебаг - структура поста:'); - console.log(' id:', serverPosts[0].id); - console.log(' image field:', serverPosts[0].image); - console.log(' slug:', serverPosts[0].slug); - console.log(''); - } - - console.log(`📊 На сервере: ${serverPosts.length} постов\n`); - - console.log('📥 Получение локальных постов...'); - const localPosts = await localPb.collection('posts').getFullList(); - const localSlugs = new Set(localPosts.map(p => p.slug)); - const localPostsMap = new Map(localPosts.map(p => [p.slug, p])); - - console.log(`📊 Локально: ${localPosts.length} постов\n`); - - // Создаем карту slug -> localId для маппинга файлов - const slugToLocalId = new Map(localPosts.map(p => [p.slug, p.id])); - - let added = 0; - let skipped = 0; - let imagesDownloaded = 0; - - for (const post of serverPosts) { - if (localSlugs.has(post.slug)) { - // Пост уже есть - проверяем, есть ли картинка - const localPost = localPostsMap.get(post.slug); - if (!localPost?.image && post.image) { - console.log(`📷 Загружаю изображение для: ${post.slug}`); - - // Маппим server record ID -> local record ID через slug - const localRecordId = slugToLocalId.get(post.slug); - if (!localRecordId) { - console.log(` ❌ Не найден local ID для slug: ${post.slug}\n`); - continue; - } - - const filename = post.image; - - // Копируем файл из server storage в local storage - // Путь: D:\...\backend\pb_data\storage\pbc_1125843985\{record_id}\{filename} - // Но у нас нет доступа к server storage напрямую! - // Однако - мы можем скопировать из ЛОКАЛЬНОГО storage, если там есть эти файлы - // Они были загружены при создании постов в локальной БД - - // Проверим, есть ли файл в локальном storage - console.log(` Server record ID: ${post.id}`); - console.log(` Local record ID: ${localRecordId}`); - console.log(` Image filename: ${filename}`); - - // Так как файлы на сервере недоступны, попробуем скачать через другой метод - // Проверим есть ли они в локальном storage - const localStorageDir = path.join(LOCAL_STORAGE_PATH, localRecordId); - if (fs.existsSync(localStorageDir)) { - const files = fs.readdirSync(localStorageDir); - console.log(` Файлы в local storage: ${files.join(', ')}`); - } - } - skipped++; - continue; - } - - try { - // Загружаем изображение если есть - let localImage = ''; - if (post.image) { - const filename = post.image; - const tempPath = path.join(TEMP_DIR, filename); - const fileUrl = `${SERVER_URL}api/files/collections/posts/${post.id}/${filename}`; - - console.log(`📷 Скачиваю изображение для ${post.slug}...`); - console.log(` URL: ${fileUrl}`); - const downloaded = await downloadFile(fileUrl, tempPath, token); - - if (downloaded && fs.existsSync(tempPath)) { - try { - // Создаем запись с изображением через FormData - const formData = new FormData(); - const fileBuffer = fs.readFileSync(tempPath); - const blob = new Blob([fileBuffer]); - const file = new File([blob], filename); - formData.append('image', file); - formData.append('title', post.title); - formData.append('slug', post.slug); - formData.append('description', post.description || ''); - formData.append('content', post.content || ''); - formData.append('author', post.author || ''); - formData.append('category', post.category || ''); - formData.append('categoryColor', post.categoryColor || ''); - formData.append('date', post.date || ''); - formData.append('readTime', post.readmeTime || ''); - formData.append('draft', String(post.draft ?? false)); - formData.append('views', String(post.views ?? 0)); - - const newRecord = await localPb.collection('posts').create(formData); - localImage = newRecord.image; - - fs.unlinkSync(tempPath); - console.log(` ✅ Создан с изображением\n`); - } catch (e: any) { - console.error(` ❌ Ошибка загрузки изображения:`, e.message); - // Создаем без изображения - } - } else { - console.log(` ⚠️ Не удалось скачать изображение, создаю без него\n`); - } - } - - // Если изображение не загружено, создаем запись без него - if (!localImage) { - const postData = { - title: post.title, - slug: post.slug, - description: post.description, - content: post.content, - author: post.author, - category: post.category, - categoryColor: post.categoryColor, - date: post.date, - readTime: post.readmeTime || '', - image: '', - draft: post.draft ?? false, - views: post.views ?? 0, - }; - - await localPb.collection('posts').create(postData); - } - - console.log(`✅ Добавлен: ${post.slug}`); - added++; - } catch (e: any) { - console.error(`❌ Ошибка добавления ${post.slug}:`, e.response?.data || e.message); - } - } - - // Очистка temps - if (fs.existsSync(TEMP_DIR)) { - fs.rmSync(TEMP_DIR, { recursive: true, force: true }); - } - - console.log(`\n📊 Готово! Добавлено: ${added}, пропущено: ${skipped}, изображений загружено: ${imagesDownloaded}`); -} - -syncPosts().catch(console.error); \ No newline at end of file diff --git a/scripts/sync-posts.ts b/scripts/sync-posts.ts deleted file mode 100644 index 38b1cf4..0000000 --- a/scripts/sync-posts.ts +++ /dev/null @@ -1,158 +0,0 @@ -import PocketBase from 'pocketbase'; -import fs from 'fs'; -import path from 'path'; - -const LOCAL_URL = 'http://127.0.0.1:8090'; -const SERVER_URL = 'https://avt-back.ru/'; - -const ADMIN_EMAIL = 'redibedi2019@gmail.com'; -const ADMIN_PASSWORD = 'Stalin4444'; - -const LOCAL_STORAGE_DIR = 'D:/Verstka/production/astro_avtourist/backend/pb_data/storage/pbc_1125843985'; - -interface Post { - id: string; - title: string; - slug: string; - description: string; - content: string; - author: string; - category: string; - categoryColor: string; - date: string; - readTime: string; - readmeTime: string; - image: string; - draft: boolean; - views: number; - created: string; - updated: string; -} - -async function main() { - console.log('🔄 Синхронизация постов: Локальный → Публичный сервер\n'); - - // Подключение к локальному PocketBase - const localPb = new PocketBase(LOCAL_URL); - await localPb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); - console.log('✅ Подключено к локальной БД'); - - // Подключение к публичному серверу - const serverPb = new PocketBase(SERVER_URL); - await serverPb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); - console.log('✅ Подключено к публичному серверу\n'); - - // Получение постов с локальной БД - console.log('📥 Получение постов с локальной БД...'); - const localPosts = await localPb.collection('posts').getFullList({ - filter: 'slug != "" && title != ""', - }); - - // Фильтруем только неп черновые посты - const publishedPosts = localPosts.filter(p => !p.draft && p.slug); - - console.log(`📊 Локально: ${publishedPosts.length} опубликованных постов\n`); - - // Получение постов с публичного сервера - console.log('📥 Получение постов с публичного сервера...'); - const serverPosts = await serverPb.collection('posts').getFullList(); - const serverSlugs = new Set(serverPosts.map(p => p.slug)); - - console.log(`📊 На сервере: ${serverPosts.length} постов\n`); - - // Синхронизация постов - let created = 0; - let skipped = 0; - let imagesUploaded = 0; - - for (const post of publishedPosts) { - // Проверяем, существует ли пост на сервере - if (serverSlugs.has(post.slug)) { - console.log(`⏭️ Пропущен (уже есть): ${post.slug}`); - skipped++; - continue; - } - - console.log(`📝 Создаю пост: ${post.slug}`); - - try { - // Если есть картинка - загружаем через FormData - if (post.image) { - const imagePath = path.join(LOCAL_STORAGE_DIR, post.id, post.image); - - if (fs.existsSync(imagePath)) { - console.log(` 📷 Загружаю изображение: ${post.image}`); - - const formData = new FormData(); - const fileBuffer = fs.readFileSync(imagePath); - const blob = new Blob([fileBuffer]); - const file = new File([blob], post.image); - formData.append('image', file); - formData.append('title', post.title); - formData.append('slug', post.slug); - formData.append('description', post.description || ''); - formData.append('content', post.content || ''); - formData.append('author', post.author || ''); - formData.append('category', post.category || ''); - formData.append('categoryColor', post.categoryColor || ''); - formData.append('date', post.date || ''); - formData.append('readTime', post.readTime || ''); - formData.append('readmeTime', post.readTime || post.readmeTime || ''); - formData.append('draft', String(post.draft ?? false)); - formData.append('views', String(post.views ?? 0)); - - await serverPb.collection('posts').create(formData); - console.log(` ✅ Создан с картинкой`); - imagesUploaded++; - } else { - // Файла нет - создаем без картинки - console.log(` ⚠️ Картинка не найдена: ${imagePath}`); - await serverPb.collection('posts').create({ - title: post.title, - slug: post.slug, - description: post.description || '', - content: post.content || '', - author: post.author || '', - category: post.category || '', - categoryColor: post.categoryColor || '', - date: post.date || '', -readTime: post.readTime || '', - readmeTime: post.readTime || post.readmeTime || '', - image: '', - draft: post.draft ?? false, - views: post.views ?? 0, - }); - console.log(` ✅ Создан без картинки`); - } - } else { - // Нет картинки - создаем без неё - await serverPb.collection('posts').create({ - title: post.title, - slug: post.slug, - description: post.description || '', - content: post.content || '', - author: post.author || '', - category: post.category || '', - categoryColor: post.categoryColor || '', - date: post.date || '', - readTime: post.readTime || post.readmeTime || '', - image: '', - draft: post.draft ?? false, - views: post.views ?? 0, - }); - console.log(` ✅ Создан без картинки`); - } - - created++; - } catch (e: any) { - console.error(` ❌ Ошибка:`, e.response?.data || e.message); - } - } - - console.log('\n📊 Итоги:'); - console.log(` ✅ Создано: ${created}`); - console.log(` ⏭️ Пропущено: ${skipped}`); - console.log(` 🖼️ Картинок загружено: ${imagesUploaded}`); -} - -main().catch(console.error); \ No newline at end of file