Skip to content

Commit f5be59c

Browse files
committed
feat: Set file attributes including creation/modification time.
1 parent 61c9772 commit f5be59c

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

AMSMB2/AMSMB2.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,88 @@ public class SMB2Manager: NSObject, NSSecureCoding, Codable, NSCopying, CustomRe
512512
attributesOfItem(atPath: path, completionHandler: asyncHandler(continuation))
513513
}
514514
}
515+
516+
/**
517+
Sets the attributes of the specified file or directory.
518+
519+
- Parameters:
520+
- attributes: A dictionary containing as keys the attributes to set for path
521+
and as values the corresponding value for the attribute.
522+
You can set the following attributes: `creationDateKey`, `contentAccessDateKey`,
523+
`contentModificationDateKey`, `attributeModificationDateKey`,
524+
`isUserImmutableKey`, `isSystemImmutableKey` and `isHiddenKey`.
525+
You can change single attributes or any combination of attributes;
526+
you need not specify keys for all attributes.
527+
- path: The path of a file or directory.
528+
- completionHandler: closure will be run after operation is completed.
529+
*/
530+
open func setAttributes(attributes: [URLResourceKey: Any], ofItemAtPath path: String, completionHandler: SimpleCompletionHandler) {
531+
var stat = smb2_stat_64()
532+
var smb2Attributes = SMB2FileAttributes()
533+
for attribute in attributes {
534+
switch attribute.key {
535+
case .creationDateKey:
536+
attributes.creationDate.map(timespec.init).map {
537+
stat.smb2_btime = UInt64($0.tv_sec)
538+
stat.smb2_btime_nsec = UInt64($0.tv_nsec)
539+
}
540+
case .contentAccessDateKey:
541+
attributes.contentAccessDate.map(timespec.init).map {
542+
stat.smb2_atime = UInt64($0.tv_sec)
543+
stat.smb2_atime_nsec = UInt64($0.tv_nsec)
544+
}
545+
case .contentModificationDateKey:
546+
attributes.contentModificationDate.map(timespec.init).map {
547+
stat.smb2_mtime = UInt64($0.tv_sec)
548+
stat.smb2_mtime_nsec = UInt64($0.tv_nsec)
549+
}
550+
case .attributeModificationDateKey:
551+
attributes.contentModificationDate.map(timespec.init).map {
552+
stat.smb2_ctime = UInt64($0.tv_sec)
553+
stat.smb2_ctime_nsec = UInt64($0.tv_nsec)
554+
}
555+
case .isUserImmutableKey:
556+
guard let value = attribute.value as? Bool else { break }
557+
smb2Attributes.insert(value ? .readonly : .normal)
558+
case .isSystemImmutableKey:
559+
guard let value = attribute.value as? Bool else { break }
560+
smb2Attributes.insert(value ? .system : .normal)
561+
case .isHiddenKey:
562+
guard let value = attribute.value as? Bool else { break }
563+
smb2Attributes.insert(value ? .hidden : .normal)
564+
default:
565+
break
566+
}
567+
}
568+
569+
if smb2Attributes.subtracting(.normal) != [] {
570+
smb2Attributes.remove(.normal)
571+
}
572+
573+
with(completionHandler: completionHandler) { [stat, smb2Attributes] context in
574+
let file = try SMB2FileHandle(forUpdatingAtPath: path, on: context)
575+
try file.set(stat: stat, attributes: smb2Attributes)
576+
}
577+
}
578+
579+
/**
580+
Sets the attributes of the specified file or directory.
581+
582+
- Parameters:
583+
- attributes: A dictionary containing as keys the attributes to set for path
584+
and as values the corresponding value for the attribute.
585+
You can set the following attributes: `creationDateKey`, `contentAccessDateKey`,
586+
`contentModificationDateKey`, `attributeModificationDateKey`, `isReadableKey`,
587+
`isUserImmutableKey`, `isSystemImmutableKey` and `isHiddenKey`.
588+
You can change single attributes or any combination of attributes;
589+
you need not specify keys for all attributes.
590+
- path: The path of a file or directory.
591+
*/
592+
open func setAttributes(attributes: [URLResourceKey: Any], ofItemAtPath path: String) async throws {
593+
try await withCheckedThrowingContinuation { continuation in
594+
setAttributes(attributes: attributes, ofItemAtPath: path, completionHandler: asyncHandler(continuation))
595+
}
596+
}
515597

516598
/**
517599
Returns the path of the item pointed to by a symbolic link.

AMSMB2/Extensions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ extension Date {
170170
}
171171
}
172172

173+
extension timespec {
174+
init(_ date: Date) {
175+
let interval = date.timeIntervalSince1970
176+
self.init(tv_sec: .init(interval), tv_nsec: Int(interval.truncatingRemainder(dividingBy: 1) * Double(NSEC_PER_SEC)))
177+
}
178+
}
179+
173180
extension Data {
174181
init<T: FixedWidthInteger>(value: T) {
175182
var value = value.littleEndian

AMSMB2/FileHandle.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,36 @@ final class SMB2FileHandle {
123123
}
124124
return st
125125
}
126+
127+
func set(stat: smb2_stat_64, attributes: SMB2FileAttributes) throws {
128+
let handle = try handle.unwrap()
129+
try context.async_await_pdu(dataHandler: EmptyReply.init) {
130+
context, cbPtr -> UnsafeMutablePointer<smb2_pdu>? in
131+
var bfi = smb2_file_basic_info(
132+
creation_time: smb2_timeval(
133+
tv_sec: UInt32(stat.smb2_btime),
134+
tv_usec: UInt32(stat.smb2_btime_nsec) / 1000),
135+
last_access_time: smb2_timeval(
136+
tv_sec: UInt32(stat.smb2_atime),
137+
tv_usec: UInt32(stat.smb2_atime_nsec) / 1000),
138+
last_write_time: smb2_timeval(
139+
tv_sec: UInt32(stat.smb2_mtime),
140+
tv_usec: UInt32(stat.smb2_mtime_nsec) / 1000),
141+
change_time: smb2_timeval(
142+
tv_sec: UInt32(stat.smb2_ctime),
143+
tv_usec: UInt32(stat.smb2_ctime_nsec) / 1000),
144+
file_attributes: attributes.rawValue)
145+
146+
var req = smb2_set_info_request()
147+
req.file_id = smb2_get_file_id(handle).pointee
148+
req.info_type = UInt8(SMB2_0_INFO_FILE)
149+
req.file_info_class = UInt8(SMB2_FILE_BASIC_INFORMATION)
150+
return withUnsafeMutablePointer(to: &bfi) { bfi in
151+
req.input_data = .init(bfi)
152+
return smb2_cmd_set_info_async(context, &req, SMB2Context.generic_handler, cbPtr)
153+
}
154+
}
155+
}
126156

127157
func ftruncate(toLength: UInt64) throws {
128158
let handle = try handle.unwrap()
@@ -258,3 +288,31 @@ final class SMB2FileHandle {
258288
try fcntl(command: command, args: [])
259289
}
260290
}
291+
292+
struct SMB2FileAttributes: OptionSet, Sendable {
293+
var rawValue: UInt32
294+
295+
init(rawValue: UInt32) {
296+
self.rawValue = rawValue
297+
}
298+
299+
init(rawValue: Int32) {
300+
self.rawValue = .init(bitPattern: rawValue)
301+
}
302+
303+
static let readonly = Self(rawValue: SMB2_FILE_ATTRIBUTE_READONLY)
304+
static let hidden = Self(rawValue: SMB2_FILE_ATTRIBUTE_HIDDEN)
305+
static let system = Self(rawValue: SMB2_FILE_ATTRIBUTE_SYSTEM)
306+
static let directory = Self(rawValue: SMB2_FILE_ATTRIBUTE_DIRECTORY)
307+
static let archive = Self(rawValue: SMB2_FILE_ATTRIBUTE_ARCHIVE)
308+
static let normal = Self(rawValue: SMB2_FILE_ATTRIBUTE_NORMAL)
309+
static let temporary = Self(rawValue: SMB2_FILE_ATTRIBUTE_TEMPORARY)
310+
static let sparseFile = Self(rawValue: SMB2_FILE_ATTRIBUTE_SPARSE_FILE)
311+
static let reparsePoint = Self(rawValue: SMB2_FILE_ATTRIBUTE_REPARSE_POINT)
312+
static let compressed = Self(rawValue: SMB2_FILE_ATTRIBUTE_COMPRESSED)
313+
static let offline = Self(rawValue: SMB2_FILE_ATTRIBUTE_OFFLINE)
314+
static let notContentIndexed = Self(rawValue: SMB2_FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
315+
static let encrypted = Self(rawValue: SMB2_FILE_ATTRIBUTE_ENCRYPTED)
316+
static let integrityStream = Self(rawValue: SMB2_FILE_ATTRIBUTE_INTEGRITY_STREAM)
317+
static let noScrubData = Self(rawValue: SMB2_FILE_ATTRIBUTE_NO_SCRUB_DATA)
318+
}

AMSMB2/Parsers.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import Foundation
1111
import SMB2
1212
import SMB2.Raw
1313

14+
struct EmptyReply {
15+
init(_: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws { }
16+
}
17+
1418
extension String {
1519
init(_: SMB2Context, _ dataPtr: UnsafeMutableRawPointer?) throws {
1620
self = try String(cString: dataPtr.unwrap().assumingMemoryBound(to: Int8.self))

AMSMB2Tests/SMB2ManagerTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,36 @@ class SMB2ManagerTests: XCTestCase {
130130
fsAttributes[.systemSize] as! Int64, fsAttributes[.systemFreeSize] as! Int64
131131
)
132132
}
133+
134+
func testFileAttributes() async throws {
135+
let file = "attribstest.dat"
136+
let size: Int = random(max: 0x000800)
137+
let smb = SMB2Manager(url: server, credential: credential)!
138+
let data = randomData(size: size)
139+
140+
addTeardownBlock {
141+
try? await smb.removeFile(atPath: file)
142+
}
143+
144+
try await smb.connectShare(name: share, encrypted: encrypted)
145+
try await smb.write(data: data, toPath: file, progress: nil)
146+
147+
let initialAttribs = try await smb.attributesOfItem(atPath: file)
148+
XCTAssertNotNil(initialAttribs.name)
149+
XCTAssertNotNil(initialAttribs.contentModificationDate)
150+
XCTAssertNotNil(initialAttribs.creationDate)
151+
XCTAssertGreaterThanOrEqual(initialAttribs.contentModificationDate!, initialAttribs.creationDate!)
152+
XCTAssertEqual(initialAttribs[.isHiddenKey] as? Bool, nil)
153+
154+
try await smb.setAttributes(attributes: [
155+
.creationDateKey: Date(timeIntervalSinceReferenceDate: 0),
156+
.isHiddenKey: true,
157+
], ofItemAtPath: file)
158+
159+
let newAttribs = try await smb.attributesOfItem(atPath: file)
160+
XCTAssertEqual(initialAttribs.contentModificationDate, newAttribs.contentModificationDate)
161+
XCTAssertEqual(newAttribs.creationDate, Date(timeIntervalSinceReferenceDate: 0))
162+
}
133163

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

0 commit comments

Comments
 (0)