Skip to content

Commit a76158b

Browse files
fatbobmanclaude
andcommitted
feat: Add working directory control for XLSX generation
- Add WorkingDirectoryLocation enum with three strategies: - .alongsideOutput (default): maintains legacy behavior - .systemTemp: uses isolated temporary directory - .custom(URL): allows custom working directory - Update write() and writeAsync() methods with workingDirectory parameter - Implement smart temporary directory handling to prevent pollution - Add comprehensive documentation for working directory best practices - Maintain full backward compatibility with existing API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ec56c2b commit a76158b

File tree

4 files changed

+291
-18
lines changed

4 files changed

+291
-18
lines changed

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,78 @@ Task {
565565

566566
## 🔧 Advanced Configuration
567567

568+
### Working Directory Management
569+
570+
Objects2XLSX provides flexible control over where temporary working files are stored during XLSX generation. This is particularly useful when working with temporary directories to avoid file system pollution:
571+
572+
```swift
573+
// Default behavior - working directory alongside output file
574+
try book.write(to: outputURL)
575+
// Output: /path/to/report.xlsx
576+
// Working dir: /path/to/report.temp/
577+
578+
// System temporary directory - recommended for temp outputs
579+
try book.write(to: outputURL, workingDirectory: .systemTemp)
580+
// Output: /path/to/report.xlsx
581+
// Working dir: /tmp/Objects2XLSX_UUID/
582+
583+
// Custom working directory
584+
let workspaceURL = URL(fileURLWithPath: "/custom/workspace")
585+
try book.write(to: outputURL, workingDirectory: .custom(workspaceURL))
586+
// Output: /path/to/report.xlsx
587+
// Working dir: /custom/workspace/Objects2XLSX_UUID/
588+
```
589+
590+
**Best Practices for Temporary Directory Usage:**
591+
592+
```swift
593+
extension Book {
594+
/// Smart writing that automatically chooses appropriate working directory
595+
func writeSmartly(to url: URL) throws -> URL {
596+
let tempDir = FileManager.default.temporaryDirectory
597+
598+
if url.path.hasPrefix(tempDir.path) {
599+
// Output is in temp directory - use isolated working space
600+
return try write(to: url, workingDirectory: .systemTemp)
601+
} else {
602+
// Output is in regular directory - use default behavior
603+
return try write(to: url)
604+
}
605+
}
606+
}
607+
608+
// Usage examples
609+
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
610+
let tempURL = FileManager.default.temporaryDirectory
611+
612+
// ✅ Good: Documents directory with default behavior
613+
try book.write(to: documentsURL.appendingPathComponent("report.xlsx"))
614+
615+
// ✅ Good: Temp directory with isolated working space
616+
try book.write(
617+
to: tempURL.appendingPathComponent("report.xlsx"),
618+
workingDirectory: .systemTemp
619+
)
620+
621+
// ✅ Good: Smart automatic selection
622+
try book.writeSmartly(to: outputURL)
623+
```
624+
625+
**Working Directory Strategies:**
626+
627+
- **`.alongsideOutput` (default)**: Creates `.temp` folder next to output file
628+
- ✅ Good for: Documents, Downloads, custom directories
629+
- ⚠️ Avoid for: System temporary directories (causes pollution)
630+
631+
- **`.systemTemp`**: Uses isolated temporary directory
632+
- ✅ Good for: Any output in system temp directories
633+
- ✅ Prevents: Temporary directory structure pollution
634+
- 🚀 Recommended: When output path starts with temp directory
635+
636+
- **`.custom(URL)`**: Uses specified directory for working files
637+
- ✅ Good for: Custom build systems, specific workspace requirements
638+
- 🔧 Control: Full control over working file location
639+
568640
### Custom Sheet Styling
569641

570642
```swift

README_CN.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,78 @@ Task {
516516

517517
## 🔧 高级配置
518518

519+
### 工作目录管理
520+
521+
Objects2XLSX 提供灵活控制 XLSX 生成过程中临时工作文件存储位置的功能。这在处理临时目录时特别有用,可避免文件系统污染:
522+
523+
```swift
524+
// 默认行为 - 工作目录在输出文件旁边
525+
try book.write(to: outputURL)
526+
// 输出:/path/to/report.xlsx
527+
// 工作目录:/path/to/report.temp/
528+
529+
// 系统临时目录 - 推荐用于临时输出
530+
try book.write(to: outputURL, workingDirectory: .systemTemp)
531+
// 输出:/path/to/report.xlsx
532+
// 工作目录:/tmp/Objects2XLSX_UUID/
533+
534+
// 自定义工作目录
535+
let workspaceURL = URL(fileURLWithPath: "/custom/workspace")
536+
try book.write(to: outputURL, workingDirectory: .custom(workspaceURL))
537+
// 输出:/path/to/report.xlsx
538+
// 工作目录:/custom/workspace/Objects2XLSX_UUID/
539+
```
540+
541+
**临时目录使用最佳实践:**
542+
543+
```swift
544+
extension Book {
545+
/// 智能写入,自动选择合适的工作目录
546+
func writeSmartly(to url: URL) throws -> URL {
547+
let tempDir = FileManager.default.temporaryDirectory
548+
549+
if url.path.hasPrefix(tempDir.path) {
550+
// 输出在临时目录 - 使用隔离工作空间
551+
return try write(to: url, workingDirectory: .systemTemp)
552+
} else {
553+
// 输出在常规目录 - 使用默认行为
554+
return try write(to: url)
555+
}
556+
}
557+
}
558+
559+
// 使用示例
560+
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
561+
let tempURL = FileManager.default.temporaryDirectory
562+
563+
// ✅ 好:文档目录使用默认行为
564+
try book.write(to: documentsURL.appendingPathComponent("report.xlsx"))
565+
566+
// ✅ 好:临时目录使用隔离工作空间
567+
try book.write(
568+
to: tempURL.appendingPathComponent("report.xlsx"),
569+
workingDirectory: .systemTemp
570+
)
571+
572+
// ✅ 好:智能自动选择
573+
try book.writeSmartly(to: outputURL)
574+
```
575+
576+
**工作目录策略:**
577+
578+
- **`.alongsideOutput`(默认)**:在输出文件旁创建 `.temp` 文件夹
579+
- ✅ 适用于:文档、下载、自定义目录
580+
- ⚠️ 避免用于:系统临时目录(会造成污染)
581+
582+
- **`.systemTemp`**:使用隔离的临时目录
583+
- ✅ 适用于:系统临时目录中的任何输出
584+
- ✅ 防止:临时目录结构污染
585+
- 🚀 推荐:当输出路径以临时目录开头时
586+
587+
- **`.custom(URL)`**:使用指定目录存放工作文件
588+
- ✅ 适用于:自定义构建系统、特定工作空间要求
589+
- 🔧 控制:完全控制工作文件位置
590+
519591
### 异步数据加载与线程安全
520592

521593
Objects2XLSX 为复杂场景提供线程安全的异步数据加载:

README_JP.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,78 @@ Task {
516516

517517
## 🔧 高度な設定
518518

519+
### 作業ディレクトリ管理
520+
521+
Objects2XLSX は XLSX 生成中の一時作業ファイルの保存場所を柔軟にコントロールする機能を提供します。これは一時ディレクトリを扱う際にファイルシステムの汚染を避けるため特に有用です:
522+
523+
```swift
524+
// デフォルト動作 - 出力ファイルの隣に作業ディレクトリ
525+
try book.write(to: outputURL)
526+
// 出力:/path/to/report.xlsx
527+
// 作業ディレクトリ:/path/to/report.temp/
528+
529+
// システム一時ディレクトリ - 一時出力に推奨
530+
try book.write(to: outputURL, workingDirectory: .systemTemp)
531+
// 出力:/path/to/report.xlsx
532+
// 作業ディレクトリ:/tmp/Objects2XLSX_UUID/
533+
534+
// カスタム作業ディレクトリ
535+
let workspaceURL = URL(fileURLWithPath: "/custom/workspace")
536+
try book.write(to: outputURL, workingDirectory: .custom(workspaceURL))
537+
// 出力:/path/to/report.xlsx
538+
// 作業ディレクトリ:/custom/workspace/Objects2XLSX_UUID/
539+
```
540+
541+
**一時ディレクトリ使用のベストプラクティス:**
542+
543+
```swift
544+
extension Book {
545+
/// 適切な作業ディレクトリを自動選択するスマート書き込み
546+
func writeSmartly(to url: URL) throws -> URL {
547+
let tempDir = FileManager.default.temporaryDirectory
548+
549+
if url.path.hasPrefix(tempDir.path) {
550+
// 出力が一時ディレクトリ内 - 分離された作業スペースを使用
551+
return try write(to: url, workingDirectory: .systemTemp)
552+
} else {
553+
// 出力が通常ディレクトリ内 - デフォルト動作を使用
554+
return try write(to: url)
555+
}
556+
}
557+
}
558+
559+
// 使用例
560+
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
561+
let tempURL = FileManager.default.temporaryDirectory
562+
563+
// ✅ 良い:ドキュメントディレクトリでデフォルト動作
564+
try book.write(to: documentsURL.appendingPathComponent("report.xlsx"))
565+
566+
// ✅ 良い:一時ディレクトリで分離された作業スペース
567+
try book.write(
568+
to: tempURL.appendingPathComponent("report.xlsx"),
569+
workingDirectory: .systemTemp
570+
)
571+
572+
// ✅ 良い:スマート自動選択
573+
try book.writeSmartly(to: outputURL)
574+
```
575+
576+
**作業ディレクトリ戦略:**
577+
578+
- **`.alongsideOutput`(デフォルト)**:出力ファイルの隣に `.temp` フォルダを作成
579+
- ✅ 適用場面:ドキュメント、ダウンロード、カスタムディレクトリ
580+
- ⚠️ 避けるべき:システム一時ディレクトリ(汚染を引き起こす)
581+
582+
- **`.systemTemp`**:分離された一時ディレクトリを使用
583+
- ✅ 適用場面:システム一時ディレクトリ内の任意の出力
584+
- ✅ 防止:一時ディレクトリ構造の汚染
585+
- 🚀 推奨:出力パスが一時ディレクトリで始まる場合
586+
587+
- **`.custom(URL)`**:作業ファイル用に指定されたディレクトリを使用
588+
- ✅ 適用場面:カスタムビルドシステム、特定のワークスペース要件
589+
- 🔧 制御:作業ファイル位置の完全制御
590+
519591
### 非同期データ読み込み&スレッドセーフティ
520592

521593
Objects2XLSX は複雑なシナリオ向けのスレッドセーフな非同期データ読み込みを提供:

Sources/Objects2XLSX/Book/Book.swift

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
import Foundation
1010
import SimpleLogger
1111

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+
1223
/// Represents an Excel Workbook that contains multiple worksheets and manages XLSX file generation.
1324
///
1425
/// `Book` is the main entry point for creating XLSX files from Swift objects. It manages a
@@ -236,8 +247,11 @@ public final class Book {
236247
/// incrementally, making it suitable for generating files with thousands of rows while
237248
/// maintaining reasonable memory consumption.
238249
@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)
241255
}
242256

243257
/// Asynchronously writes the workbook to an XLSX file at the specified URL.
@@ -300,31 +314,37 @@ public final class Book {
300314
/// - Progress monitoring via `progressStream` is thread-safe and can be observed from any
301315
/// thread
302316
@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)
305322
}
306323

307324
/// Core XLSX generation logic (synchronous version)
308325
/// - Parameters:
309326
/// - url: Target URL for the XLSX file
327+
/// - workingDirectory: Working directory location strategy
310328
/// - useAsync: Whether to use async data loading (ignored in sync version)
311329
/// - Returns: The actual URL where the XLSX file was written
312330
/// - 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+
{
318336
sendProgress(.started)
319337

320338
do {
339+
// Use new location determination logic (prepareOutputURL already handles parent directory creation)
340+
let (outputURL, tempDir) = try determineOutputLocation(from: url, location: workingDirectory)
341+
321342
// Create registries for optimization
322343
let styleRegister = StyleRegister()
323344
let shareStringRegister = ShareStringRegister()
324345

325-
// Create temporary directory for building XLSX package structure
346+
// Create working directory structure
326347
sendProgress(.creatingDirectory)
327-
let tempDir = outputURL.deletingPathExtension().appendingPathExtension("temp")
328348
try createXLSXDirectoryStructure(at: tempDir)
329349

330350
// Process sheets and collect metadata
@@ -355,24 +375,27 @@ public final class Book {
355375
/// Core XLSX generation logic (asynchronous version)
356376
/// - Parameters:
357377
/// - url: Target URL for the XLSX file
378+
/// - workingDirectory: Working directory location strategy
358379
/// - useAsync: Whether to use async data loading
359380
/// - Returns: The actual URL where the XLSX file was written
360381
/// - 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+
{
366387
sendProgress(.started)
367388

368389
do {
390+
// Use new location determination logic (prepareOutputURL already handles parent directory creation)
391+
let (outputURL, tempDir) = try determineOutputLocation(from: url, location: workingDirectory)
392+
369393
// Create registries for optimization
370394
let styleRegister = StyleRegister()
371395
let shareStringRegister = ShareStringRegister()
372396

373-
// Create temporary directory for building XLSX package structure
397+
// Create working directory structure
374398
sendProgress(.creatingDirectory)
375-
let tempDir = outputURL.deletingPathExtension().appendingPathExtension("temp")
376399
try createXLSXDirectoryStructure(at: tempDir)
377400

378401
// Process sheets and collect metadata (async version)
@@ -608,6 +631,40 @@ public final class Book {
608631
return outputURL
609632
}
610633

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+
611668
/// Ensures the URL has a .xlsx extension
612669
/// - Parameter url: The input URL
613670
/// - Returns: URL with .xlsx extension (replaces existing extension if different)

0 commit comments

Comments
 (0)