1 // Copyright 2022-present 650 Industries. All rights reserved.
2 
3 import Foundation
4 import OSLog
5 
6 import ExpoModulesCore
7 
8 /**
9  Class to read expo-updates logs using OSLogReader
10  */
11 internal final class UpdatesLogReader {
12   private let serialQueue = DispatchQueue(label: "dev.expo.updates.logging.reader")
13   private let logPersistence = PersistentFileLog(category: UpdatesLogger.EXPO_UPDATES_LOG_CATEGORY)
14 
15   /**
16    Get expo-updates logs newer than the given date
17    Returns the log entries unpacked as dictionaries
18    Maximum of one day lookback is allowed
19    */
20   func getLogEntries(newerThan: Date) throws -> [[String: Any]] {
21     let epoch = epochFromDateOrOneDayAgo(date: newerThan)
22     return logPersistence.readEntries()
23       .compactMap { entryString in
24         logStringToFilteredLogEntry(entryString: entryString, epoch: epoch)?.asDict()
25       }
26   }
27 
28   /**
29    Get expo-updates logs newer than the given date
30    Returned strings are all in the JSON format of UpdatesLogEntry
31    Maximum of one day lookback is allowed
32    */
33   func getLogEntries(newerThan: Date) -> [String] {
34     let epoch = epochFromDateOrOneDayAgo(date: newerThan)
35     return logPersistence.readEntries()
36       .compactMap { entryString in
37         logStringToFilteredLogEntry(entryString: entryString, epoch: epoch)?.asString()
38       }
39   }
40 
41   /**
42    Purge all log entries written more than one day ago
43    */
44   func purgeLogEntries(completion: @escaping (Error?) -> Void) {
45     purgeLogEntries(
46       olderThan: Date().addingTimeInterval(-UpdatesLogReader.MAXIMUM_LOOKBACK_INTERVAL),
47       completion: completion
48     )
49   }
50 
51   /**
52    Purge all log entries written prior to the given date
53    */
54   func purgeLogEntries(olderThan: Date, completion: @escaping (Error?) -> Void) {
55     let epoch = epochFromDateOrOneDayAgo(date: olderThan)
56     logPersistence.purgeEntriesNotMatchingFilter(filter: { entryString in
57       self.logStringToFilteredLogEntry(entryString: entryString, epoch: epoch) != nil
58     }, {error in
59       completion(error)
60     })
61   }
62 
63   private func logStringToFilteredLogEntry(entryString: String, epoch: UInt) -> UpdatesLogEntry? {
64     if entryString.lengthOfBytes(using: .utf8) < 2 {
65       return nil
66     }
67     let suffixFrom = entryString.index(entryString.startIndex, offsetBy: 2)
68     let entryStringSuffix = String(entryString.suffix(from: suffixFrom))
69     let entry = UpdatesLogEntry.create(from: entryStringSuffix)
70     return entry?.timestamp ?? 0 >= epoch ? entry : nil
71   }
72 
73   private static let MAXIMUM_LOOKBACK_INTERVAL: TimeInterval = 86_400 // 1 day
74 
75   private func epochFromDateOrOneDayAgo(date: Date) -> UInt {
76     // Returns the epoch (milliseconds since 1/1/1970)
77     // If date is earlier than one day ago, then the epoch for one day ago is returned
78     // instead
79     let earliestDate = Date().addingTimeInterval(-UpdatesLogReader.MAXIMUM_LOOKBACK_INTERVAL)
80     let dateToUse = date.timeIntervalSince1970 < earliestDate.timeIntervalSince1970 ?
81       earliestDate :
82       date
83     return UInt(dateToUse.timeIntervalSince1970) * 1000
84   }
85 }
86