Skip to content

Commit

Permalink
Follow symlinks when reading a file's attributes (#642)
Browse files Browse the repository at this point in the history
* Follow symlinks when reading a file's attributes

* Fix up the formatting
  • Loading branch information
Joannis authored Dec 28, 2024
1 parent 2ce29ad commit 53c231f
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 7 deletions.
16 changes: 14 additions & 2 deletions Sources/Hummingbird/Files/FileIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public struct FileIO: Sendable {
chunkLength: Int = NonBlockingFileIO.defaultChunkSize
) async throws -> ResponseBody {
do {
let stat = try await fileIO.lstat(path: path)
let stat = try await fileIO.stat(path: path)
guard stat.st_size > 0 else { return .init() }
return self.readFile(path: path, range: 0...numericCast(stat.st_size - 1), context: context, chunkLength: chunkLength)
} catch {
Expand All @@ -67,7 +67,7 @@ public struct FileIO: Sendable {
chunkLength: Int = NonBlockingFileIO.defaultChunkSize
) async throws -> ResponseBody {
do {
let stat = try await fileIO.lstat(path: path)
let stat = try await fileIO.stat(path: path)
guard stat.st_size > 0 else { return .init() }
let fileRange: ClosedRange<Int> = 0...numericCast(stat.st_size - 1)
let range = range.clamped(to: fileRange)
Expand Down Expand Up @@ -144,3 +144,15 @@ public struct FileIO: Sendable {
}
}
}

extension NonBlockingFileIO {
func stat(path: String) async throws -> stat {
let stat = try await self.lstat(path: path)
if stat.st_mode & S_IFMT == S_IFLNK {
let realPath = try await self.readlink(path: path)
return try await self.lstat(path: realPath)
} else {
return stat
}
}
}
10 changes: 5 additions & 5 deletions Sources/Hummingbird/Files/LocalFileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@ public struct LocalFileSystem: FileProvider {
/// - Returns: File attributes
public func getAttributes(id path: FileIdentifier) async throws -> FileAttributes? {
do {
let lstat = try await self.fileIO.fileIO.lstat(path: path)
let isFolder = (lstat.st_mode & S_IFMT) == S_IFDIR
let stat = try await self.fileIO.fileIO.stat(path: path)
let isFolder = (stat.st_mode & S_IFMT) == S_IFDIR
#if os(Linux)
let modificationDate = Double(lstat.st_mtim.tv_sec) + (Double(lstat.st_mtim.tv_nsec) / 1_000_000_000.0)
let modificationDate = Double(stat.st_mtim.tv_sec) + (Double(stat.st_mtim.tv_nsec) / 1_000_000_000.0)
#else
let modificationDate = Double(lstat.st_mtimespec.tv_sec) + (Double(lstat.st_mtimespec.tv_nsec) / 1_000_000_000.0)
let modificationDate = Double(stat.st_mtimespec.tv_sec) + (Double(stat.st_mtimespec.tv_nsec) / 1_000_000_000.0)
#endif
return .init(
isFolder: isFolder,
size: numericCast(lstat.st_size),
size: numericCast(stat.st_size),
modificationDate: Date(timeIntervalSince1970: modificationDate)
)
} catch {
Expand Down
38 changes: 38 additions & 0 deletions Tests/HummingbirdTests/FileMiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Foundation
import HTTPTypes
import Hummingbird
import HummingbirdTesting
import NIOPosix
import XCTest

final class FileMiddlewareTests: XCTestCase {
Expand Down Expand Up @@ -317,6 +318,43 @@ final class FileMiddlewareTests: XCTestCase {
}
}

func testSymlink() async throws {
let router = Router()
router.middlewares.add(FileMiddleware(".", searchForIndexHtml: true))
let app = Application(responder: router.buildResponder())

let text = "Test file contents"
let data = Data(text.utf8)
let fileURL = URL(fileURLWithPath: "test.html")
XCTAssertNoThrow(try data.write(to: fileURL))
defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) }

let fileIO = NonBlockingFileIO(threadPool: .singleton)

try await app.test(.router) { client in
try await client.execute(uri: "/test.html", method: .get) { response in
XCTAssertEqual(String(buffer: response.body), text)
}

try await client.execute(uri: "/", method: .get) { response in
XCTAssertEqual(String(buffer: response.body), "")
}

try await fileIO.symlink(path: "index.html", to: "test.html")

do {
try await client.execute(uri: "/", method: .get) { response in
XCTAssertEqual(String(buffer: response.body), text)
}

try await fileIO.unlink(path: "index.html")
} catch {
try await fileIO.unlink(path: "index.html")
throw error
}
}
}

func testOnThrowCustom404() async throws {
let router = Router()
router.middlewares.add(FileMiddleware(".", searchForIndexHtml: true))
Expand Down

0 comments on commit 53c231f

Please sign in to comment.