Skip to content

Commit

Permalink
feat: Set file attributes including creation/modification time.
Browse files Browse the repository at this point in the history
  • Loading branch information
amosavian committed Nov 21, 2023
1 parent 61c9772 commit f5be59c
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
82 changes: 82 additions & 0 deletions AMSMB2/AMSMB2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,88 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe
attributesOfItem(atPath: path, completionHandler: asyncHandler(continuation))
}
}

/**
Sets the attributes of the specified file or directory.
- Parameters:
- attributes: A dictionary containing as keys the attributes to set for path
and as values the corresponding value for the attribute.
You can set the following attributes: `creationDateKey`, `contentAccessDateKey`,
`contentModificationDateKey`, `attributeModificationDateKey`,
`isUserImmutableKey`, `isSystemImmutableKey` and `isHiddenKey`.
You can change single attributes or any combination of attributes;
you need not specify keys for all attributes.
- path: The path of a file or directory.
- completionHandler: closure will be run after operation is completed.
*/
open func setAttributes(attributes: [URLResourceKey: Any], ofItemAtPath path: String, completionHandler: SimpleCompletionHandler) {
var stat = smb2_stat_64()
var smb2Attributes = SMB2FileAttributes()
for attribute in attributes {
switch attribute.key {
case .creationDateKey:
attributes.creationDate.map(timespec.init).map {
stat.smb2_btime = UInt64($0.tv_sec)
stat.smb2_btime_nsec = UInt64($0.tv_nsec)
}
case .contentAccessDateKey:
attributes.contentAccessDate.map(timespec.init).map {
stat.smb2_atime = UInt64($0.tv_sec)
stat.smb2_atime_nsec = UInt64($0.tv_nsec)
}
case .contentModificationDateKey:
attributes.contentModificationDate.map(timespec.init).map {
stat.smb2_mtime = UInt64($0.tv_sec)
stat.smb2_mtime_nsec = UInt64($0.tv_nsec)
}
case .attributeModificationDateKey:
attributes.contentModificationDate.map(timespec.init).map {
stat.smb2_ctime = UInt64($0.tv_sec)
stat.smb2_ctime_nsec = UInt64($0.tv_nsec)
}
case .isUserImmutableKey:
guard let value = attribute.value as? Bool else { break }
smb2Attributes.insert(value ? .readonly : .normal)
case .isSystemImmutableKey:
guard let value = attribute.value as? Bool else { break }
smb2Attributes.insert(value ? .system : .normal)
case .isHiddenKey:
guard let value = attribute.value as? Bool else { break }
smb2Attributes.insert(value ? .hidden : .normal)
default:
break
}
}

if smb2Attributes.subtracting(.normal) != [] {
smb2Attributes.remove(.normal)
}

with(completionHandler: completionHandler) { [stat, smb2Attributes] context in
let file = try SMB2FileHandle(forUpdatingAtPath: path, on: context)
try file.set(stat: stat, attributes: smb2Attributes)
}
}

/**
Sets the attributes of the specified file or directory.
- Parameters:
- attributes: A dictionary containing as keys the attributes to set for path
and as values the corresponding value for the attribute.
You can set the following attributes: `creationDateKey`, `contentAccessDateKey`,
`contentModificationDateKey`, `attributeModificationDateKey`, `isReadableKey`,
`isUserImmutableKey`, `isSystemImmutableKey` and `isHiddenKey`.
You can change single attributes or any combination of attributes;
you need not specify keys for all attributes.
- path: The path of a file or directory.
*/
open func setAttributes(attributes: [URLResourceKey: Any], ofItemAtPath path: String) async throws {
try await withCheckedThrowingContinuation { continuation in
setAttributes(attributes: attributes, ofItemAtPath: path, completionHandler: asyncHandler(continuation))
}
}

/**
Returns the path of the item pointed to by a symbolic link.
Expand Down
7 changes: 7 additions & 0 deletions AMSMB2/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ extension Date {
}
}

extension timespec {
init(_ date: Date) {
let interval = date.timeIntervalSince1970
self.init(tv_sec: .init(interval), tv_nsec: Int(interval.truncatingRemainder(dividingBy: 1) * Double(NSEC_PER_SEC)))
}
}

extension Data {
init<T: FixedWidthInteger>(value: T) {
var value = value.littleEndian
Expand Down
58 changes: 58 additions & 0 deletions AMSMB2/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,36 @@ final class SMB2FileHandle {
}
return st
}

func set(stat: smb2_stat_64, attributes: SMB2FileAttributes) throws {
let handle = try handle.unwrap()
try context.async_await_pdu(dataHandler: EmptyReply.init) {
context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
var bfi = smb2_file_basic_info(
creation_time: smb2_timeval(
tv_sec: UInt32(stat.smb2_btime),
tv_usec: UInt32(stat.smb2_btime_nsec) / 1000),
last_access_time: smb2_timeval(
tv_sec: UInt32(stat.smb2_atime),
tv_usec: UInt32(stat.smb2_atime_nsec) / 1000),
last_write_time: smb2_timeval(
tv_sec: UInt32(stat.smb2_mtime),
tv_usec: UInt32(stat.smb2_mtime_nsec) / 1000),
change_time: smb2_timeval(
tv_sec: UInt32(stat.smb2_ctime),
tv_usec: UInt32(stat.smb2_ctime_nsec) / 1000),
file_attributes: attributes.rawValue)

var req = smb2_set_info_request()
req.file_id = smb2_get_file_id(handle).pointee
req.info_type = UInt8(SMB2_0_INFO_FILE)
req.file_info_class = UInt8(SMB2_FILE_BASIC_INFORMATION)
return withUnsafeMutablePointer(to: &bfi) { bfi in
req.input_data = .init(bfi)
return smb2_cmd_set_info_async(context, &req, SMB2Context.generic_handler, cbPtr)
}
}
}

func ftruncate(toLength: UInt64) throws {
let handle = try handle.unwrap()
Expand Down Expand Up @@ -258,3 +288,31 @@ final class SMB2FileHandle {
try fcntl(command: command, args: [])
}
}

struct SMB2FileAttributes: OptionSet, Sendable {
var rawValue: UInt32

init(rawValue: UInt32) {
self.rawValue = rawValue
}

init(rawValue: Int32) {
self.rawValue = .init(bitPattern: rawValue)
}

static let readonly = Self(rawValue: SMB2_FILE_ATTRIBUTE_READONLY)
static let hidden = Self(rawValue: SMB2_FILE_ATTRIBUTE_HIDDEN)
static let system = Self(rawValue: SMB2_FILE_ATTRIBUTE_SYSTEM)
static let directory = Self(rawValue: SMB2_FILE_ATTRIBUTE_DIRECTORY)
static let archive = Self(rawValue: SMB2_FILE_ATTRIBUTE_ARCHIVE)
static let normal = Self(rawValue: SMB2_FILE_ATTRIBUTE_NORMAL)
static let temporary = Self(rawValue: SMB2_FILE_ATTRIBUTE_TEMPORARY)
static let sparseFile = Self(rawValue: SMB2_FILE_ATTRIBUTE_SPARSE_FILE)
static let reparsePoint = Self(rawValue: SMB2_FILE_ATTRIBUTE_REPARSE_POINT)
static let compressed = Self(rawValue: SMB2_FILE_ATTRIBUTE_COMPRESSED)
static let offline = Self(rawValue: SMB2_FILE_ATTRIBUTE_OFFLINE)
static let notContentIndexed = Self(rawValue: SMB2_FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
static let encrypted = Self(rawValue: SMB2_FILE_ATTRIBUTE_ENCRYPTED)
static let integrityStream = Self(rawValue: SMB2_FILE_ATTRIBUTE_INTEGRITY_STREAM)
static let noScrubData = Self(rawValue: SMB2_FILE_ATTRIBUTE_NO_SCRUB_DATA)
}
4 changes: 4 additions & 0 deletions AMSMB2/Parsers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import Foundation
import SMB2
import SMB2.Raw

struct EmptyReply {
init(_: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws { }
}

extension String {
init(_: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws {
self = try String(cString: dataPtr.unwrap().assumingMemoryBound(to: Int8.self))
Expand Down
30 changes: 30 additions & 0 deletions AMSMB2Tests/SMB2ManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,36 @@ class SMB2ManagerTests: XCTestCase {
fsAttributes[.systemSize] as! Int64, fsAttributes[.systemFreeSize] as! Int64
)
}

func testFileAttributes() async throws {
let file = "attribstest.dat"
let size: Int = random(max: 0x000800)
let smb = SMB2Manager(url: server, credential: credential)!
let data = randomData(size: size)

addTeardownBlock {
try? await smb.removeFile(atPath: file)
}

try await smb.connectShare(name: share, encrypted: encrypted)
try await smb.write(data: data, toPath: file, progress: nil)

let initialAttribs = try await smb.attributesOfItem(atPath: file)
XCTAssertNotNil(initialAttribs.name)
XCTAssertNotNil(initialAttribs.contentModificationDate)
XCTAssertNotNil(initialAttribs.creationDate)
XCTAssertGreaterThanOrEqual(initialAttribs.contentModificationDate!, initialAttribs.creationDate!)
XCTAssertEqual(initialAttribs[.isHiddenKey] as? Bool, nil)

try await smb.setAttributes(attributes: [
.creationDateKey: Date(timeIntervalSinceReferenceDate: 0),
.isHiddenKey: true,
], ofItemAtPath: file)

let newAttribs = try await smb.attributesOfItem(atPath: file)
XCTAssertEqual(initialAttribs.contentModificationDate, newAttribs.contentModificationDate)
XCTAssertEqual(newAttribs.creationDate, Date(timeIntervalSinceReferenceDate: 0))
}

func testListing() async throws {
let smb = SMB2Manager(url: server, credential: credential)!
Expand Down

0 comments on commit f5be59c

Please sign in to comment.