feat(remote): 添加 CORS 中间件支持文件跨域访问
This commit is contained in:
@@ -20,7 +20,7 @@ export const config = {
|
||||
},
|
||||
|
||||
get tempRoot(): string {
|
||||
return path.join(os.tmpdir(), 'xcnote_uploads')
|
||||
return path.join(os.tmpdir(), 'xcdesktop_uploads')
|
||||
},
|
||||
|
||||
get serverPort(): number {
|
||||
|
||||
@@ -32,6 +32,110 @@ import { logger } from '../../utils/logger.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get(
|
||||
'/drives',
|
||||
asyncHandler(async (_req: Request, res: Response) => {
|
||||
const drives: FileItemDTO[] = []
|
||||
const letters = 'CDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
|
||||
for (const letter of letters) {
|
||||
const drivePath = `${letter}:\\`
|
||||
try {
|
||||
await fs.access(drivePath)
|
||||
drives.push({
|
||||
name: `${letter}:`,
|
||||
type: 'dir',
|
||||
size: 0,
|
||||
modified: new Date().toISOString(),
|
||||
path: drivePath,
|
||||
})
|
||||
} catch {
|
||||
// 驱动器不存在,跳过
|
||||
}
|
||||
}
|
||||
|
||||
successResponse(res, { items: drives })
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/system',
|
||||
validateQuery(listFilesQuerySchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const systemPath = req.query.path as string
|
||||
if (!systemPath) {
|
||||
throw new BadRequestError('路径不能为空')
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(systemPath)
|
||||
|
||||
try {
|
||||
await fs.access(fullPath)
|
||||
} catch {
|
||||
throw new NotFoundError('路径不存在')
|
||||
}
|
||||
|
||||
const stats = await fs.stat(fullPath)
|
||||
if (!stats.isDirectory()) {
|
||||
throw new NotADirectoryError()
|
||||
}
|
||||
|
||||
const files = await fs.readdir(fullPath)
|
||||
const items = await Promise.all(
|
||||
files.map(async (name): Promise<FileItemDTO | null> => {
|
||||
const filePath = path.join(fullPath, name)
|
||||
try {
|
||||
const fileStats = await fs.stat(filePath)
|
||||
return {
|
||||
name,
|
||||
type: fileStats.isDirectory() ? 'dir' : 'file',
|
||||
size: fileStats.size,
|
||||
modified: fileStats.mtime.toISOString(),
|
||||
path: filePath,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const visibleItems = items.filter((i): i is FileItemDTO => i !== null && !i.name.startsWith('.'))
|
||||
visibleItems.sort((a, b) => {
|
||||
if (a.type === b.type) return a.name.localeCompare(b.name)
|
||||
return a.type === 'dir' ? -1 : 1
|
||||
})
|
||||
|
||||
successResponse(res, { items: visibleItems })
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/system/content',
|
||||
validateQuery(contentQuerySchema),
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const systemPath = req.query.path as string
|
||||
if (!systemPath) {
|
||||
throw new BadRequestError('路径不能为空')
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(systemPath)
|
||||
const stats = await fs.stat(fullPath).catch(() => {
|
||||
throw new NotFoundError('文件不存在')
|
||||
})
|
||||
|
||||
if (!stats.isFile()) throw new BadRequestError('不是文件')
|
||||
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
successResponse(res, {
|
||||
content,
|
||||
metadata: {
|
||||
size: stats.size,
|
||||
modified: stats.mtime.toISOString(),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
validateQuery(listFilesQuerySchema),
|
||||
|
||||
@@ -182,7 +182,9 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
const filePath = getMonthFilePath(year, month)
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
const data = JSON.parse(content)
|
||||
data.activeDays = Object.values(data.days).filter(d => d.totalDuration > 0).length
|
||||
return data
|
||||
} catch (err) {
|
||||
return createEmptyMonthData(year, month)
|
||||
}
|
||||
@@ -192,7 +194,9 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
const filePath = getYearFilePath(year)
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
const data = JSON.parse(content)
|
||||
data.totalActiveDays = Object.values(data.months).filter(m => m.totalDuration > 0).length
|
||||
return data
|
||||
} catch (err) {
|
||||
return createEmptyYearData(year)
|
||||
}
|
||||
@@ -226,7 +230,7 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
if (existingSessionIndex >= 0) {
|
||||
const oldDuration = dayData.sessions[existingSessionIndex].duration
|
||||
dayData.sessions[existingSessionIndex] = realtimeSession
|
||||
dayData.totalDuration += currentSessionDuration - oldDuration
|
||||
dayData.totalDuration = dayData.totalDuration - oldDuration + currentSessionDuration
|
||||
} else {
|
||||
dayData.sessions.push(realtimeSession)
|
||||
dayData.totalDuration += currentSessionDuration
|
||||
@@ -269,8 +273,10 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
monthData.days[dayStr].totalDuration += duration
|
||||
monthData.days[dayStr].sessions += 1
|
||||
monthData.monthlyTotal += duration
|
||||
monthData.activeDays = Object.keys(monthData.days).length
|
||||
monthData.averageDaily = Math.floor(monthData.monthlyTotal / monthData.activeDays)
|
||||
monthData.activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length
|
||||
monthData.averageDaily = monthData.activeDays > 0
|
||||
? Math.floor(monthData.monthlyTotal / monthData.activeDays)
|
||||
: 0
|
||||
monthData.lastUpdated = new Date().toISOString()
|
||||
|
||||
await fs.writeFile(filePath, JSON.stringify(monthData, null, 2), 'utf-8')
|
||||
@@ -289,10 +295,15 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
|
||||
yearData.months[monthStr].totalDuration += duration
|
||||
yearData.yearlyTotal += duration
|
||||
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0)
|
||||
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => {
|
||||
const hasActiveDays = m.totalDuration > 0 ? 1 : 0
|
||||
return sum + hasActiveDays
|
||||
}, 0)
|
||||
|
||||
const monthCount = Object.keys(yearData.months).length
|
||||
yearData.averageMonthly = Math.floor(yearData.yearlyTotal / monthCount)
|
||||
const activeMonthCount = Object.values(yearData.months).filter(m => m.totalDuration > 0).length
|
||||
yearData.averageMonthly = activeMonthCount > 0
|
||||
? Math.floor(yearData.yearlyTotal / activeMonthCount)
|
||||
: 0
|
||||
yearData.averageDaily = yearData.totalActiveDays > 0
|
||||
? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays)
|
||||
: 0
|
||||
@@ -315,7 +326,7 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
const oldDayDuration = monthData.days[dayStr].totalDuration
|
||||
monthData.days[dayStr].totalDuration = todayDuration
|
||||
monthData.monthlyTotal = monthData.monthlyTotal - oldDayDuration + todayDuration
|
||||
monthData.activeDays = Object.keys(monthData.days).length
|
||||
monthData.activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length
|
||||
monthData.averageDaily = monthData.activeDays > 0
|
||||
? Math.floor(monthData.monthlyTotal / monthData.activeDays)
|
||||
: 0
|
||||
@@ -345,10 +356,15 @@ class SessionPersistenceService implements SessionPersistence {
|
||||
yearData.months[monthStr].totalDuration = monthData.monthlyTotal
|
||||
yearData.months[monthStr].activeDays = monthData.activeDays
|
||||
yearData.yearlyTotal = yearData.yearlyTotal - oldMonthTotal + monthData.monthlyTotal
|
||||
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0)
|
||||
yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => {
|
||||
const hasActiveDays = m.totalDuration > 0 ? 1 : 0
|
||||
return sum + hasActiveDays
|
||||
}, 0)
|
||||
|
||||
const monthCount = Object.keys(yearData.months).length
|
||||
yearData.averageMonthly = monthCount > 0 ? Math.floor(yearData.yearlyTotal / monthCount) : 0
|
||||
const activeMonthCount = Object.values(yearData.months).filter(m => m.totalDuration > 0).length
|
||||
yearData.averageMonthly = activeMonthCount > 0
|
||||
? Math.floor(yearData.yearlyTotal / activeMonthCount)
|
||||
: 0
|
||||
yearData.averageDaily = yearData.totalActiveDays > 0
|
||||
? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays)
|
||||
: 0
|
||||
|
||||
@@ -366,23 +366,47 @@ class TimeTrackerService {
|
||||
if (targetMonth) {
|
||||
const monthData = await this.persistence.getMonthData(targetYear, targetMonth)
|
||||
totalDuration = monthData.monthlyTotal
|
||||
activeDays = monthData.activeDays
|
||||
activeDays = Object.values(monthData.days).filter(d => d.totalDuration > 0).length
|
||||
|
||||
for (const [day, summary] of Object.entries(monthData.days)) {
|
||||
if (!longestDay || summary.totalDuration > longestDay.duration) {
|
||||
longestDay = { date: `${targetYear}-${targetMonth.toString().padStart(2, '0')}-${day}`, duration: summary.totalDuration }
|
||||
}
|
||||
for (const tab of summary.topTabs || []) {
|
||||
tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration
|
||||
}
|
||||
}
|
||||
|
||||
const dayData = await this.persistence.getDayData(targetYear, targetMonth, 1)
|
||||
for (const session of dayData.sessions) {
|
||||
for (const record of session.tabRecords) {
|
||||
const key = record.filePath || record.fileName
|
||||
tabDurations[key] = (tabDurations[key] || 0) + record.duration
|
||||
tabTypeDurations[record.tabType] = (tabTypeDurations[record.tabType] || 0) + record.duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const yearData = await this.persistence.getYearData(targetYear)
|
||||
totalDuration = yearData.yearlyTotal
|
||||
activeDays = yearData.totalActiveDays
|
||||
activeDays = Object.values(yearData.months).reduce((sum, m) => {
|
||||
return sum + Object.entries(m).filter(([_, d]) => (d as { totalDuration: number }).totalDuration > 0).length
|
||||
}, 0)
|
||||
|
||||
for (const [month, summary] of Object.entries(yearData.months)) {
|
||||
if (!longestDay || summary.totalDuration > longestDay.duration) {
|
||||
longestDay = { date: `${targetYear}-${month}`, duration: summary.totalDuration }
|
||||
}
|
||||
}
|
||||
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
const monthStr = m.toString().padStart(2, '0')
|
||||
const monthData = await this.persistence.getMonthData(targetYear, m)
|
||||
for (const dayData of Object.values(monthData.days)) {
|
||||
for (const tab of dayData.topTabs || []) {
|
||||
tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user