Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ var/
sdist/
develop-eggs/
.installed.cfg
lib/
lib64/
*.egg

Expand Down
74 changes: 74 additions & 0 deletions app/api/storage/download/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextResponse } from 'next/server'
import fs from 'fs/promises'
import path from 'path'

export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const filePath = searchParams.get('path')

if (!filePath) {
return NextResponse.json(
{ success: false, error: 'No file path provided' },
{ status: 400 }
)
}

// Security check to ensure the path is within the storage directory
const storagePath = path.join(process.cwd(), 'storage/markdown')
const normalizedPath = path.normalize(filePath)
if (!normalizedPath.startsWith(storagePath)) {
return NextResponse.json(
{ success: false, error: 'Invalid file path' },
{ status: 403 }
)
}

// Check if file exists
try {
await fs.access(normalizedPath)
} catch {
return NextResponse.json(
{ success: false, error: 'File not found' },
{ status: 404 }
)
}

// Read the file
const content = await fs.readFile(normalizedPath, 'utf-8')

// If it's a JSON file, verify it's valid JSON
if (path.extname(filePath) === '.json') {
try {
JSON.parse(content)
} catch {
return NextResponse.json(
{ success: false, error: 'Invalid JSON file' },
{ status: 500 }
)
}
}

// Determine content type based on file extension
const contentType = path.extname(filePath) === '.json'
? 'application/json'
: 'text/markdown'

// Create response with appropriate headers for download
return new NextResponse(content, {
headers: {
'Content-Type': contentType,
'Content-Disposition': `attachment; filename="${path.basename(filePath)}"`,
},
})
} catch (error) {
console.error('Error downloading file:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to download file'
},
{ status: 500 }
)
}
}
168 changes: 70 additions & 98 deletions app/api/storage/route.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,96 @@
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import { NextResponse } from 'next/server'
import fs from 'fs/promises'
import path from 'path'
import { URL } from 'url'

const STORAGE_DIR = path.join(process.cwd(), 'storage', 'markdown')
const STORAGE_DIR = path.join(process.cwd(), 'storage/markdown')

// Ensure storage directory exists
if (!fs.existsSync(STORAGE_DIR)) {
fs.mkdirSync(STORAGE_DIR, { recursive: true })
}

function getFilenameFromUrl(url: string): string {
try {
const parsedUrl = new URL(url)
// Use hostname and pathname to create a unique filename
const filename = `${parsedUrl.hostname}${parsedUrl.pathname.replace(/\//g, '_')}`
.replace(/[^a-zA-Z0-9-_]/g, '_') // Replace invalid chars with underscore
.replace(/_+/g, '_') // Replace multiple underscores with single
.toLowerCase()
return `${filename}.json`
} catch (error) {
// Fallback for invalid URLs
return `${url.replace(/[^a-zA-Z0-9-_]/g, '_')}.json`
}
}

// POST /api/storage - Save markdown
export async function POST(request: NextRequest) {
export async function POST(request: Request) {
try {
const { url, content, stats } = await request.json()
const filename = getFilenameFromUrl(url)
const filepath = path.join(STORAGE_DIR, filename)

const data = {
url,
content,
timestamp: new Date().toISOString(),
stats
}

// Save JSON file
fs.writeFileSync(filepath, JSON.stringify(data, null, 2))
const { url, content } = await request.json()

// Create storage directory if it doesn't exist
await fs.mkdir(STORAGE_DIR, { recursive: true })

// Generate filename from URL
const filename = url
.replace(/^https?:\/\//, '')
.replace(/[^a-z0-9]/gi, '_')
.toLowerCase() + '.md'
const filePath = path.join(STORAGE_DIR, filename)
await fs.writeFile(filePath, content, 'utf-8')

// Save markdown file
const markdownPath = filepath.replace('.json', '.md')
fs.writeFileSync(markdownPath, content)

return NextResponse.json({ success: true })
} catch (error) {
console.error('Error saving markdown:', error)
return NextResponse.json(
{ error: 'Failed to save markdown' },
{ success: false, error: error instanceof Error ? error.message : 'Failed to save markdown' },
{ status: 500 }
)
}
}

// GET /api/storage - List files
// GET /api/storage?url=... - Load specific file
export async function GET(request: NextRequest) {
export async function GET(request: Request) {
try {
const url = request.nextUrl.searchParams.get('url')
const { searchParams } = new URL(request.url)
const url = searchParams.get('url')

// If no URL provided, list all files
// Handle list request
if (!url) {
const files = fs.readdirSync(STORAGE_DIR)
.filter(file => file.endsWith('.json'))
.map(file => {
const name = file.replace('.json', '')
const jsonPath = `storage/markdown/${file}`
const markdownPath = `storage/markdown/${name}.md`
// Only get .md files
const files = await fs.readdir(STORAGE_DIR)
const mdFiles = files.filter(f => f.endsWith('.md'))

const fileDetails = await Promise.all(
mdFiles.map(async (filename) => {
const mdPath = path.join(STORAGE_DIR, filename)
const jsonPath = path.join(STORAGE_DIR, filename.replace('.md', '.json'))
const stats = await fs.stat(mdPath)
const content = await fs.readFile(mdPath, 'utf-8')

// Only include if both files exist
if (fs.existsSync(path.join(process.cwd(), jsonPath)) &&
fs.existsSync(path.join(process.cwd(), markdownPath))) {
// Get file stats
const stats = fs.statSync(path.join(process.cwd(), jsonPath))
const content = JSON.parse(fs.readFileSync(path.join(process.cwd(), jsonPath), 'utf-8'))

// Clean up the name by removing common prefixes and file extensions
const cleanName = name
.replace(/^docs[._]/, '') // Remove leading docs prefix
.replace(/\.json$/, '') // Remove .json extension
.replace(/\.md$/, '') // Remove .md extension

return {
name: cleanName, // Use cleaned name
jsonPath,
markdownPath,
timestamp: stats.mtime, // Keep as Date for sorting
size: stats.size,
wordCount: content.stats?.wordCount || 0,
charCount: content.stats?.charCount || 0
}
// Create JSON file if it doesn't exist
if (!files.includes(filename.replace('.md', '.json'))) {
const jsonContent = JSON.stringify({
content,
metadata: {
wordCount: content.split(/\s+/).length,
charCount: content.length,
timestamp: stats.mtime
}
}, null, 2)
await fs.writeFile(jsonPath, jsonContent, 'utf-8')
}

return {
name: filename.replace('.md', ''),
jsonPath,
markdownPath: mdPath,
timestamp: stats.mtime,
size: stats.size,
wordCount: content.split(/\s+/).length,
charCount: content.length
}
return null
})
.filter((file): file is NonNullable<typeof file> => file !== null) // Type-safe filter
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) // Sort by newest first

return NextResponse.json({ files })
}

// Load specific file
const filename = getFilenameFromUrl(url)
const filepath = path.join(STORAGE_DIR, filename)

if (!fs.existsSync(filepath)) {
return NextResponse.json(
{ error: 'No stored content found' },
{ status: 404 }
)

return NextResponse.json({
success: true,
files: fileDetails
})
}

const content = fs.readFileSync(filepath, 'utf-8')
return NextResponse.json(JSON.parse(content))

// Handle single file request
const filename = url
.replace(/^https?:\/\//, '')
.replace(/[^a-z0-9]/gi, '_')
.toLowerCase() + '.md'

const filePath = path.join(STORAGE_DIR, filename)
const content = await fs.readFile(filePath, 'utf-8')

return NextResponse.json({ success: true, content })
} catch (error) {
console.error('Error loading markdown:', error)
return NextResponse.json(
{ error: 'Failed to load markdown' },
{ success: false, error: error instanceof Error ? error.message : 'Failed to load markdown' },
{ status: 500 }
)
}
Expand Down
15 changes: 12 additions & 3 deletions components/StoredFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@ export default function StoredFiles() {
const listFiles = async () => {
try {
const response = await fetch('/api/storage')
if (!response.ok) throw new Error('Failed to fetch files')
const data = await response.json()
setFiles(data.files)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to fetch files')
}

const { success, files } = await response.json()
if (!success || !files) {
throw new Error('Invalid response format')
}

setFiles(files)
} catch (error) {
console.error('Error loading stored files:', error)
setFiles([])
} finally {
setIsLoading(false)
}
Expand Down
35 changes: 35 additions & 0 deletions lib/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export async function saveMarkdown(url: string, content: string) {
try {
const response = await fetch('/api/storage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, content }),
})

if (!response.ok) {
throw new Error('Failed to save markdown')
}

return await response.json()
} catch (error) {
console.error('Error saving markdown:', error)
return { success: false, error: error instanceof Error ? error.message : 'Failed to save markdown' }
}
}

export async function loadMarkdown(url: string) {
try {
const response = await fetch(`/api/storage?url=${encodeURIComponent(url)}`)

if (!response.ok) {
throw new Error('Failed to load markdown')
}

return await response.json()
} catch (error) {
console.error('Error loading markdown:', error)
return { success: false, error: error instanceof Error ? error.message : 'Failed to load markdown' }
}
}