forked from SwiftyBeaver/SwiftyBeaver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFileDestination.swift
240 lines (213 loc) · 9.05 KB
/
FileDestination.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//
// FileDestination.swift
// SwiftyBeaver
//
// Created by Sebastian Kreutzberger on 05.12.15.
// Copyright © 2015 Sebastian Kreutzberger
// Some rights reserved: http://opensource.org/licenses/MIT
//
import Foundation
open class FileDestination: BaseDestination {
public var logFileURL: URL?
public var syncAfterEachWrite: Bool = false
public var colored: Bool = false {
didSet {
if colored {
// bash font color, first value is intensity, second is color
// see http://bit.ly/1Otu3Zr & for syntax http://bit.ly/1Tp6Fw9
// uses the 256-color table from http://bit.ly/1W1qJuH
reset = "\u{001b}[0m"
escape = "\u{001b}[38;5;"
levelColor.verbose = "251m" // silver
levelColor.debug = "35m" // green
levelColor.info = "38m" // blue
levelColor.warning = "178m" // yellow
levelColor.error = "197m" // red
} else {
reset = ""
escape = ""
levelColor.verbose = ""
levelColor.debug = ""
levelColor.info = ""
levelColor.warning = ""
levelColor.error = ""
}
}
}
// LOGFILE ROTATION
// ho many bytes should a logfile have until it is rotated?
// default is 5 MB. Just is used if logFileAmount > 1
public var logFileMaxSize = (5 * 1024 * 1024)
// Number of log files used in rotation, default is 1 which deactivates file rotation
public var logFileAmount = 1
override public var defaultHashValue: Int {return 2}
let fileManager = FileManager.default
public init(logFileURL: URL? = nil) {
if let logFileURL = logFileURL {
self.logFileURL = logFileURL
super.init()
return
}
// platform-dependent logfile directory default
var baseURL: URL?
#if os(OSX)
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
baseURL = url
// try to use ~/Library/Caches/APP NAME instead of ~/Library/Caches
if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String {
do {
if let appURL = baseURL?.appendingPathComponent(appName, isDirectory: true) {
try fileManager.createDirectory(at: appURL,
withIntermediateDirectories: true, attributes: nil)
baseURL = appURL
}
} catch {
print("Warning! Could not create folder /Library/Caches/\(appName)")
}
}
}
#else
#if os(Linux)
baseURL = URL(fileURLWithPath: "/var/cache")
#else
// iOS, watchOS, etc. are using the caches directory
if let url = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
baseURL = url
}
#endif
#endif
if let baseURL = baseURL {
self.logFileURL = baseURL.appendingPathComponent("swiftybeaver.log", isDirectory: false)
}
super.init()
}
// append to file. uses full base class functionality
override open func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
file: String, function: String, line: Int, context: Any? = nil) -> String? {
let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context)
if let str = formattedString {
_ = validateSaveFile(str: str)
}
return formattedString
}
// check if filesize is bigger than wanted and if yes then rotate them
func validateSaveFile(str: String) -> Bool {
if self.logFileAmount > 1 {
guard let url = logFileURL else { return false }
let filePath = url.path
if FileManager.default.fileExists(atPath: filePath) == true {
do {
// Get file size
let attr = try FileManager.default.attributesOfItem(atPath: filePath)
let fileSize = attr[FileAttributeKey.size] as! UInt64
// Do file rotation
if fileSize > logFileMaxSize {
rotateFile(url)
}
} catch {
print("validateSaveFile error: \(error)")
}
}
}
return saveToFile(str: str)
}
private func rotateFile(_ fileUrl: URL) {
let filePath = fileUrl.path
let lastIndex = (logFileAmount-1)
let firstIndex = 1
do {
for index in stride(from: lastIndex, through: firstIndex, by: -1) {
let oldFile = makeRotatedFileUrl(fileUrl, index: index).path
if FileManager.default.fileExists(atPath: oldFile) {
if index == lastIndex {
// Delete the last file
try FileManager.default.removeItem(atPath: oldFile)
} else {
// Move the current file to next index
let newFile = makeRotatedFileUrl(fileUrl, index: index + 1).path
try FileManager.default.moveItem(atPath: oldFile, toPath: newFile)
}
}
}
// Finally, move the current file
let newFile = makeRotatedFileUrl(fileUrl, index: firstIndex).path
try FileManager.default.moveItem(atPath: filePath, toPath: newFile)
} catch {
print("rotateFile error: \(error)")
}
}
private func makeRotatedFileUrl(_ fileUrl: URL, index: Int) -> URL {
// The index is appended to the file name, to preserve the original extension.
fileUrl.deletingPathExtension()
.appendingPathExtension("\(index).\(fileUrl.pathExtension)")
}
/// appends a string as line to a file.
/// returns boolean about success
func saveToFile(str: String) -> Bool {
guard let url = logFileURL else { return false }
let line = str + "\n"
guard let data = line.data(using: String.Encoding.utf8) else { return false }
return write(data: data, to: url)
}
private func write(data: Data, to url: URL) -> Bool {
#if os(Linux)
return true
#else
var success = false
let coordinator = NSFileCoordinator(filePresenter: nil)
var error: NSError?
coordinator.coordinate(writingItemAt: url, error: &error) { url in
do {
if fileManager.fileExists(atPath: url.path) == false {
let directoryURL = url.deletingLastPathComponent()
if fileManager.fileExists(atPath: directoryURL.path) == false {
try fileManager.createDirectory(
at: directoryURL,
withIntermediateDirectories: true
)
}
fileManager.createFile(atPath: url.path, contents: nil)
#if os(iOS) || os(watchOS)
if #available(iOS 10.0, watchOS 3.0, *) {
var attributes = try fileManager.attributesOfItem(atPath: url.path)
attributes[FileAttributeKey.protectionKey] = FileProtectionType.none
try fileManager.setAttributes(attributes, ofItemAtPath: url.path)
}
#endif
}
let fileHandle = try FileHandle(forWritingTo: url)
fileHandle.seekToEndOfFile()
if #available(iOS 13.4, watchOS 6.2, tvOS 13.4, macOS 10.15.4, *) {
try fileHandle.write(contentsOf: data)
} else {
fileHandle.write(data)
}
if syncAfterEachWrite {
fileHandle.synchronizeFile()
}
fileHandle.closeFile()
success = true
} catch {
print("SwiftyBeaver File Destination could not write to file \(url).")
}
}
if let error = error {
print("Failed writing file with error: \(String(describing: error))")
return false
}
return success
#endif
}
/// deletes log file.
/// returns true if file was removed or does not exist, false otherwise
public func deleteLogFile() -> Bool {
guard let url = logFileURL, fileManager.fileExists(atPath: url.path) == true else { return true }
do {
try fileManager.removeItem(at: url)
return true
} catch {
print("SwiftyBeaver File Destination could not remove file \(url).")
return false
}
}
}