|
9 | 9 | import Foundation |
10 | 10 | import SimpleLogger |
11 | 11 |
|
| 12 | +/// Specifies where the temporary working directory should be located during XLSX generation |
| 13 | +/// Note: Output file location is always determined by the URL provided to write() method |
| 14 | +public enum WorkingDirectoryLocation: Sendable { |
| 15 | + /// Create temporary working directory alongside the output file (default, matches legacy behavior) |
| 16 | + case alongsideOutput |
| 17 | + /// Use system temporary directory for working files |
| 18 | + case systemTemp |
| 19 | + /// Use custom directory for working files |
| 20 | + case custom(URL) |
| 21 | +} |
| 22 | + |
12 | 23 | /// Represents an Excel Workbook that contains multiple worksheets and manages XLSX file generation. |
13 | 24 | /// |
14 | 25 | /// `Book` is the main entry point for creating XLSX files from Swift objects. It manages a |
@@ -236,8 +247,11 @@ public final class Book { |
236 | 247 | /// incrementally, making it suitable for generating files with thousands of rows while |
237 | 248 | /// maintaining reasonable memory consumption. |
238 | 249 | @discardableResult |
239 | | - public func write(to url: URL) throws(BookError) -> URL { |
240 | | - try generateXLSX(to: url, useAsync: false) |
| 250 | + public func write( |
| 251 | + to url: URL, |
| 252 | + workingDirectory: WorkingDirectoryLocation = .alongsideOutput) throws(BookError) -> URL |
| 253 | + { |
| 254 | + try generateXLSX(to: url, workingDirectory: workingDirectory, useAsync: false) |
241 | 255 | } |
242 | 256 |
|
243 | 257 | /// Asynchronously writes the workbook to an XLSX file at the specified URL. |
@@ -300,31 +314,37 @@ public final class Book { |
300 | 314 | /// - Progress monitoring via `progressStream` is thread-safe and can be observed from any |
301 | 315 | /// thread |
302 | 316 | @discardableResult |
303 | | - public func writeAsync(to url: URL) async throws(BookError) -> URL { |
304 | | - try await generateXLSXAsync(to: url, useAsync: true) |
| 317 | + public func writeAsync( |
| 318 | + to url: URL, |
| 319 | + workingDirectory: WorkingDirectoryLocation = .alongsideOutput) async throws(BookError) -> URL |
| 320 | + { |
| 321 | + try await generateXLSXAsync(to: url, workingDirectory: workingDirectory, useAsync: true) |
305 | 322 | } |
306 | 323 |
|
307 | 324 | /// Core XLSX generation logic (synchronous version) |
308 | 325 | /// - Parameters: |
309 | 326 | /// - url: Target URL for the XLSX file |
| 327 | + /// - workingDirectory: Working directory location strategy |
310 | 328 | /// - useAsync: Whether to use async data loading (ignored in sync version) |
311 | 329 | /// - Returns: The actual URL where the XLSX file was written |
312 | 330 | /// - Throws: BookError if generation fails |
313 | | - private func generateXLSX(to url: URL, useAsync: Bool) throws(BookError) -> URL { |
314 | | - // Ensure the URL has proper .xlsx extension and directory structure |
315 | | - let outputURL = try prepareOutputURL(url) |
316 | | - |
317 | | - // Begin progress reporting |
| 331 | + private func generateXLSX( |
| 332 | + to url: URL, |
| 333 | + workingDirectory: WorkingDirectoryLocation, |
| 334 | + useAsync: Bool) throws(BookError) -> URL |
| 335 | + { |
318 | 336 | sendProgress(.started) |
319 | 337 |
|
320 | 338 | do { |
| 339 | + // Use new location determination logic (prepareOutputURL already handles parent directory creation) |
| 340 | + let (outputURL, tempDir) = try determineOutputLocation(from: url, location: workingDirectory) |
| 341 | + |
321 | 342 | // Create registries for optimization |
322 | 343 | let styleRegister = StyleRegister() |
323 | 344 | let shareStringRegister = ShareStringRegister() |
324 | 345 |
|
325 | | - // Create temporary directory for building XLSX package structure |
| 346 | + // Create working directory structure |
326 | 347 | sendProgress(.creatingDirectory) |
327 | | - let tempDir = outputURL.deletingPathExtension().appendingPathExtension("temp") |
328 | 348 | try createXLSXDirectoryStructure(at: tempDir) |
329 | 349 |
|
330 | 350 | // Process sheets and collect metadata |
@@ -355,24 +375,27 @@ public final class Book { |
355 | 375 | /// Core XLSX generation logic (asynchronous version) |
356 | 376 | /// - Parameters: |
357 | 377 | /// - url: Target URL for the XLSX file |
| 378 | + /// - workingDirectory: Working directory location strategy |
358 | 379 | /// - useAsync: Whether to use async data loading |
359 | 380 | /// - Returns: The actual URL where the XLSX file was written |
360 | 381 | /// - Throws: BookError if generation fails |
361 | | - private func generateXLSXAsync(to url: URL, useAsync: Bool) async throws(BookError) -> URL { |
362 | | - // Ensure the URL has proper .xlsx extension and directory structure |
363 | | - let outputURL = try prepareOutputURL(url) |
364 | | - |
365 | | - // Begin progress reporting |
| 382 | + private func generateXLSXAsync( |
| 383 | + to url: URL, |
| 384 | + workingDirectory: WorkingDirectoryLocation, |
| 385 | + useAsync: Bool) async throws(BookError) -> URL |
| 386 | + { |
366 | 387 | sendProgress(.started) |
367 | 388 |
|
368 | 389 | do { |
| 390 | + // Use new location determination logic (prepareOutputURL already handles parent directory creation) |
| 391 | + let (outputURL, tempDir) = try determineOutputLocation(from: url, location: workingDirectory) |
| 392 | + |
369 | 393 | // Create registries for optimization |
370 | 394 | let styleRegister = StyleRegister() |
371 | 395 | let shareStringRegister = ShareStringRegister() |
372 | 396 |
|
373 | | - // Create temporary directory for building XLSX package structure |
| 397 | + // Create working directory structure |
374 | 398 | sendProgress(.creatingDirectory) |
375 | | - let tempDir = outputURL.deletingPathExtension().appendingPathExtension("temp") |
376 | 399 | try createXLSXDirectoryStructure(at: tempDir) |
377 | 400 |
|
378 | 401 | // Process sheets and collect metadata (async version) |
@@ -608,6 +631,40 @@ public final class Book { |
608 | 631 | return outputURL |
609 | 632 | } |
610 | 633 |
|
| 634 | + /// Determines output location and working directory based on strategy |
| 635 | + /// - Parameters: |
| 636 | + /// - url: Original URL provided by user (output file location is always determined by this URL) |
| 637 | + /// - location: Working directory location strategy (only affects temporary working directory) |
| 638 | + /// - Returns: Tuple containing final output URL and working directory URL |
| 639 | + /// - Throws: BookError.fileWriteError if directory operations fail |
| 640 | + private func determineOutputLocation( |
| 641 | + from url: URL, |
| 642 | + location: WorkingDirectoryLocation) throws(BookError) -> (outputURL: URL, tempDir: URL) |
| 643 | + { |
| 644 | + // Output file location is ALWAYS determined by the user-provided URL |
| 645 | + // This ensures Foundation URL behavior is respected (relative paths resolve to current directory) |
| 646 | + let outputURL = try prepareOutputURL(url) |
| 647 | + |
| 648 | + // Working directory location is determined by the strategy |
| 649 | + let tempDir: URL |
| 650 | + switch location { |
| 651 | + case .alongsideOutput: |
| 652 | + // Create temporary working directory alongside the output file (legacy behavior) |
| 653 | + tempDir = outputURL.deletingPathExtension().appendingPathExtension("temp") |
| 654 | + |
| 655 | + case .systemTemp: |
| 656 | + // Use system temporary directory for working files |
| 657 | + tempDir = FileManager.default.temporaryDirectory |
| 658 | + .appendingPathComponent("Objects2XLSX_\(UUID().uuidString)") |
| 659 | + |
| 660 | + case let .custom(customDir): |
| 661 | + // Use custom directory for working files |
| 662 | + tempDir = customDir.appendingPathComponent("Objects2XLSX_\(UUID().uuidString)") |
| 663 | + } |
| 664 | + |
| 665 | + return (outputURL, tempDir) |
| 666 | + } |
| 667 | + |
611 | 668 | /// Ensures the URL has a .xlsx extension |
612 | 669 | /// - Parameter url: The input URL |
613 | 670 | /// - Returns: URL with .xlsx extension (replaces existing extension if different) |
|
0 commit comments