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
20 changes: 0 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"dotenv": "^16.0.0",
"fastify": "^5.8.3",
"fastify-plugin": "^5.1.0",
"fs-extra": "^10.0.1",
"fs-xattr": "0.3.1",
"ioredis": "^5.2.4",
"ip-address": "^10.0.1",
Expand All @@ -91,7 +90,6 @@
"@types/async-retry": "^1.4.5",
"@types/busboy": "^1.3.0",
"@types/cloneable-readable": "^2.0.3",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.5",
"@types/json-bigint": "^1.0.4",
Expand Down
27 changes: 27 additions & 0 deletions src/internal/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as fs from 'node:fs/promises'
import path from 'node:path'

export async function ensureDir(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true })
}

export async function ensureFile(filePath: string): Promise<void> {
await ensureDir(path.dirname(filePath))

// Open in append mode so missing files are created without truncating existing ones.
const handle = await fs.open(filePath, 'a')
await handle.close()
}

export async function pathExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}

export async function removePath(filePath: string): Promise<void> {
await fs.rm(filePath, { recursive: true, force: true })
}
49 changes: 24 additions & 25 deletions src/storage/backend/file.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Stats } from 'node:fs'
import fs from 'node:fs'
import * as fsp from 'node:fs/promises'
import { ERRORS, StorageBackendError } from '@internal/errors'
import { ensureDir, ensureFile, pathExists, removePath } from '@internal/fs'
import { createHash, randomUUID } from 'crypto'
import fs from 'fs-extra'
import fsExtra from 'fs-extra'
import * as xattr from 'fs-xattr'
import path from 'path'
import stream from 'stream'
Expand Down Expand Up @@ -84,7 +86,7 @@ export class FileBackend implements StorageBackendAdapter {
): Promise<ObjectResponse> {
// 'Range: bytes=#######-######
const file = this.resolveSecurePath(withOptionalVersion(`${bucketName}/${key}`, version))
const data = await fs.stat(file)
const data = await fsp.stat(file)
const eTag = await this.etag(file, data)
const fileSize = data.size
const { cacheControl, contentType } = await this.getFileMetadata(file)
Expand Down Expand Up @@ -186,7 +188,7 @@ export class FileBackend implements StorageBackendAdapter {
): Promise<ObjectMetadata> {
try {
const file = this.resolveSecurePath(withOptionalVersion(`${bucketName}/${key}`, version))
await fs.ensureFile(file)
await ensureFile(file)
const destFile = fs.createWriteStream(file)
await pipeline(body, destFile)

Expand Down Expand Up @@ -218,7 +220,7 @@ export class FileBackend implements StorageBackendAdapter {
async deleteObject(bucket: string, key: string, version: string | undefined): Promise<void> {
try {
const file = this.resolveSecurePath(withOptionalVersion(`${bucket}/${key}`, version))
await fs.remove(file)
await removePath(file)

// Clean up empty parent directories
await this.cleanupEmptyDirectories(path.dirname(file))
Expand Down Expand Up @@ -254,13 +256,13 @@ export class FileBackend implements StorageBackendAdapter {
withOptionalVersion(`${bucket}/${destination}`, destinationVersion)
)

await fs.ensureFile(destFile)
await fs.copyFile(srcFile, destFile)
await ensureFile(destFile)
await fsp.copyFile(srcFile, destFile)

const originalMetadata = await this.getFileMetadata(srcFile)
await this.setFileMetadata(destFile, Object.assign({}, originalMetadata, metadata))

const fileStat = await fs.lstat(destFile)
const fileStat = await fsp.lstat(destFile)
const eTag = await this.etag(destFile, fileStat)

return {
Expand All @@ -277,7 +279,7 @@ export class FileBackend implements StorageBackendAdapter {
*/
async deleteObjects(bucket: string, prefixes: string[]): Promise<void> {
const promises = prefixes.map((prefix) => {
return fs.rm(this.resolveSecurePath(`${bucket}/${prefix}`))
return removePath(this.resolveSecurePath(`${bucket}/${prefix}`))
})
const results = await Promise.allSettled(promises)

Expand All @@ -286,9 +288,6 @@ export class FileBackend implements StorageBackendAdapter {

results.forEach((result, index) => {
if (result.status === 'rejected') {
if (result.reason.code === 'ENOENT') {
return
}
throw result.reason
} else {
// Add parent directory of successfully deleted file
Expand Down Expand Up @@ -320,7 +319,7 @@ export class FileBackend implements StorageBackendAdapter {
): Promise<ObjectMetadata> {
const file = this.resolveSecurePath(withOptionalVersion(`${bucket}/${key}`, version))

const data = await fs.stat(file)
const data = await fsp.stat(file)
const { cacheControl, contentType } = await this.getFileMetadata(file)
const lastModified = data.mtime
const eTag = await this.etag(file, data)
Expand Down Expand Up @@ -356,8 +355,8 @@ export class FileBackend implements StorageBackendAdapter {
'metadata.json'
)
)
await fsExtra.ensureDir(multiPartFolder)
await fsExtra.writeFile(multipartFile, JSON.stringify({ contentType, cacheControl }))
await ensureDir(multiPartFolder)
await fsp.writeFile(multipartFile, JSON.stringify({ contentType, cacheControl }))

return uploadId
}
Expand All @@ -380,7 +379,7 @@ export class FileBackend implements StorageBackendAdapter {
)
)

const writeStream = fsExtra.createWriteStream(partPath)
const writeStream = fs.createWriteStream(partPath)

await pipeline(body, writeStream)

Expand Down Expand Up @@ -415,7 +414,7 @@ export class FileBackend implements StorageBackendAdapter {
`part-${part.PartNumber}`
)
)
const partExists = await fsExtra.pathExists(partFilePath)
const partExists = await pathExists(partFilePath)

if (partExists) {
const platform = process.platform === 'darwin' ? 'darwin' : 'linux'
Expand All @@ -433,7 +432,7 @@ export class FileBackend implements StorageBackendAdapter {
finalParts.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]))

const multipartStream = this.mergePartStreams(finalParts)
const metadataContent = await fsExtra.readFile(
const metadataContent = await fsp.readFile(
this.resolveSecurePath(
path.join(
'multiparts',
Expand All @@ -457,7 +456,7 @@ export class FileBackend implements StorageBackendAdapter {
metadata.cacheControl
)

fsExtra.remove(this.resolveSecurePath(path.join('multiparts', uploadId))).catch(() => {
removePath(this.resolveSecurePath(path.join('multiparts', uploadId))).catch(() => {
// no-op
})

Expand All @@ -477,7 +476,7 @@ export class FileBackend implements StorageBackendAdapter {
): Promise<void> {
const multiPartFolder = this.resolveSecurePath(path.join('multiparts', uploadId))

await fsExtra.remove(multiPartFolder)
await removePath(multiPartFolder)

// Clean up empty parent directories
try {
Expand Down Expand Up @@ -523,7 +522,7 @@ export class FileBackend implements StorageBackendAdapter {
const etag = await this.computeMd5(partFilePath)
await this.setMetadataAttr(partFilePath, METADATA_ATTR_KEYS[platform]['etag'], etag)

const fileStat = await fs.lstat(partFilePath)
const fileStat = await fsp.lstat(partFilePath)

return {
eTag: etag,
Expand Down Expand Up @@ -607,7 +606,7 @@ export class FileBackend implements StorageBackendAdapter {
*/
protected async isEmptyDirectory(dirPath: string): Promise<boolean> {
try {
const directory = await fs.opendir(dirPath)
const directory = await fsp.opendir(dirPath)
const entry = await directory.read()
await directory.close()

Expand All @@ -629,7 +628,7 @@ export class FileBackend implements StorageBackendAdapter {
}

// Check if directory exists
const exists = await fs.pathExists(dirPath)
const exists = await pathExists(dirPath)
if (!exists) {
return
}
Expand All @@ -638,7 +637,7 @@ export class FileBackend implements StorageBackendAdapter {
const isEmpty = await this.isEmptyDirectory(dirPath)
if (isEmpty) {
// Remove empty directory - using fs.remove for better cross-platform compatibility
await fs.remove(dirPath)
await removePath(dirPath)

// Recursively check parent directory
const parentDir = path.dirname(dirPath)
Expand Down Expand Up @@ -694,7 +693,7 @@ export class FileBackend implements StorageBackendAdapter {
return normalizedPath
}

private async etag(file: string, stats: fs.Stats): Promise<string> {
private async etag(file: string, stats: Stats): Promise<string> {
if (this.etagAlgorithm === 'md5') {
const checksum = await this.computeMd5(file)
return `"${checksum}"`
Expand Down
4 changes: 2 additions & 2 deletions src/storage/protocols/tus/file-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ensureFile } from '@internal/fs'
import { Configstore, FileStore as TusFileStore } from '@tus/file-store'
import { Upload } from '@tus/server'
import fsExtra from 'fs-extra'
import path from 'path'
import { FileBackend } from '../../backend'

Expand All @@ -20,7 +20,7 @@ export class FileStore extends TusFileStore {

async create(file: Upload): Promise<Upload> {
const filePath = path.join(this.options.directory, file.id)
await fsExtra.ensureFile(filePath)
await ensureFile(filePath)

await this.fileAdapter.setFileMetadata(filePath, {
cacheControl: file.metadata?.cacheControl || '',
Expand Down
Loading