multifilebuffer.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /*
  2. * Public Domain Software
  3. *
  4. * I (Matthias Ladkau) am the author of the source code in this file.
  5. * I have placed the source code in this file in the public domain.
  6. *
  7. * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
  8. */
  9. package fileutil
  10. import (
  11. "fmt"
  12. "io/ioutil"
  13. "os"
  14. "path"
  15. "sort"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "devt.de/krotik/common/timeutil"
  20. )
  21. /*
  22. MultiFileBuffer is a file-persitent buffer which can be split over multiple files.
  23. A specified file is opened and used as backend storage for a byte buffer. By
  24. default, the file grows indefinitely. It is possible to specify a rollover
  25. condition to allow the file to rollover once the condition is satisfied.
  26. If the condition is satisfied, the file is closed and a new file is silently
  27. opened for output. The buffer will save old log files by appending the
  28. extensions ‘.1’, ‘.2’ etc., to the file name. The rollover condition is only
  29. checked once at the beginning of a write operation.
  30. For example, with a base file name of app.log, the buffer would create
  31. app.log, app.log.1, app.log.2, etc. The file being written to is always app.log.
  32. When this file is filled, it is closed and renamed to app.log.1, and if files
  33. app.log.1, app.log.2, etc. exist, then they are renamed to app.log.2, app.log.3
  34. etc. respectively.
  35. */
  36. type MultiFileBuffer struct {
  37. lock *sync.Mutex // Lock for reading and writing
  38. filename string // File name for buffer
  39. basename string // Base file name (file name + iterator decoration)
  40. iterator FilenameIterator // Iterator for file names
  41. cond RolloverCondition // Rollover condition
  42. fp *os.File // Current file handle
  43. }
  44. /*
  45. NewMultiFileBuffer creates a new MultiFileBuffer with a given file name
  46. iterator and rollover condition.
  47. */
  48. func NewMultiFileBuffer(filename string, it FilenameIterator, cond RolloverCondition) (*MultiFileBuffer, error) {
  49. var err error
  50. mfb := &MultiFileBuffer{&sync.Mutex{}, filename, it.Basename(filename), it, cond, nil}
  51. if err = mfb.checkrollover(); err != nil {
  52. return nil, err
  53. }
  54. if mfb.fp == nil {
  55. // File existed and can be continued
  56. mfb.lock.Lock()
  57. mfb.fp, err = os.OpenFile(mfb.basename, os.O_APPEND|os.O_RDWR, 0660)
  58. mfb.lock.Unlock()
  59. }
  60. return mfb, nil
  61. }
  62. /*
  63. Write writes len(p) bytes from p to the underlying data stream. It returns
  64. the number of bytes written from p (0 <= n <= len(p)) and any error
  65. encountered that caused the write to stop early.
  66. */
  67. func (mfb *MultiFileBuffer) Write(output []byte) (int, error) {
  68. var b int
  69. err := mfb.checkrollover()
  70. if err == nil {
  71. if mfb.fp == nil {
  72. // File existed and can be continued
  73. mfb.lock.Lock()
  74. mfb.fp, err = os.OpenFile(mfb.basename, os.O_APPEND|os.O_RDWR, 0660)
  75. mfb.lock.Unlock()
  76. }
  77. mfb.lock.Lock()
  78. b, err = mfb.fp.Write(output)
  79. mfb.lock.Unlock()
  80. }
  81. return b, err
  82. }
  83. /*
  84. checkrollover checks if the buffer files should be switched.
  85. */
  86. func (mfb *MultiFileBuffer) checkrollover() error {
  87. mfb.lock.Lock()
  88. defer mfb.lock.Unlock()
  89. // Update basename here
  90. mfb.basename = mfb.iterator.Basename(mfb.filename)
  91. // Rollover if the base file does not exist
  92. ex, err := PathExists(mfb.basename)
  93. if err == nil && (!ex || mfb.cond.CheckRollover(mfb.basename)) {
  94. // Rollover if either the base file does not exist or the
  95. // rollover condition is satisfied
  96. err = mfb.rollover()
  97. }
  98. return err
  99. }
  100. /*
  101. Close closes the buffer.
  102. */
  103. func (mfb *MultiFileBuffer) Close() error {
  104. var err error
  105. if mfb.fp != nil {
  106. err = mfb.fp.Close()
  107. mfb.fp = nil
  108. }
  109. return err
  110. }
  111. /*
  112. rollover switches the buffer files.
  113. */
  114. func (mfb *MultiFileBuffer) rollover() error {
  115. var err error
  116. // Recursive file renaming function
  117. var ensureFileSlot func(fn string) error
  118. ensureFileSlot = func(fn string) error {
  119. // Check if the file exists already
  120. ex, err := PathExists(fn)
  121. if ex && err == nil {
  122. // Determine new file name
  123. newfn := mfb.iterator.NextName(fn)
  124. if newfn == "" {
  125. // If it is the end of the iteration just delete the file
  126. err = os.Remove(fn)
  127. } else {
  128. // Ensure the new file name is usable
  129. err = ensureFileSlot(newfn)
  130. // Rename file according to iterator.NextName()
  131. if err == nil {
  132. err = os.Rename(fn, newfn)
  133. }
  134. }
  135. }
  136. return err
  137. }
  138. // Close existing file
  139. err = mfb.Close()
  140. // Create file handle
  141. if err == nil {
  142. err = ensureFileSlot(mfb.basename)
  143. if err == nil {
  144. // Overwrite existing base file
  145. mfb.fp, err = os.OpenFile(mfb.basename, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660)
  146. }
  147. }
  148. return err
  149. }
  150. // Rollover conditions
  151. // ===================
  152. /*
  153. RolloverCondition is used by the MultiFileBuffer to check if the buffer files
  154. should be switched.
  155. */
  156. type RolloverCondition interface {
  157. /*
  158. CheckRollover checks if the buffer files should be switched.
  159. */
  160. CheckRollover(basename string) bool
  161. }
  162. /*
  163. EmptyRolloverCondition creates a rollover condition which is never true.
  164. */
  165. func EmptyRolloverCondition() RolloverCondition {
  166. return &emptyRolloverCondition{}
  167. }
  168. /*
  169. emptyRolloverCondition is a rollover condition which is never true.
  170. */
  171. type emptyRolloverCondition struct {
  172. }
  173. /*
  174. NextName returns the next file name based on the current file name.
  175. An empty string means the end of the iteration.
  176. */
  177. func (rc *emptyRolloverCondition) CheckRollover(basename string) bool {
  178. return false
  179. }
  180. /*
  181. SizeBasedRolloverCondition creates a new rollover condition based on file
  182. size. The condition is satisfied if the base file exceeds a certain file size.
  183. */
  184. func SizeBasedRolloverCondition(maxSize int64) RolloverCondition {
  185. return &sizeBasedRolloverCondition{maxSize}
  186. }
  187. /*
  188. sizeBasedRolloverCondition is the implementation of the size based rollover
  189. condition.
  190. */
  191. type sizeBasedRolloverCondition struct {
  192. maxSize int64
  193. }
  194. /*
  195. NextName returns the next file name based on the current file name.
  196. An empty string means the end of the iteration.
  197. */
  198. func (rc *sizeBasedRolloverCondition) CheckRollover(basename string) bool {
  199. ret := false
  200. if info, err := os.Stat(basename); err == nil {
  201. ret = info.Size() >= rc.maxSize
  202. }
  203. return ret
  204. }
  205. // FilenameIterator
  206. // ================
  207. /*
  208. FilenameIterator is used by the MultiFileBuffer to determine the new file name
  209. when rotating the buffer files. Basename is called before doing any calculation.
  210. This function should do general filename decoration. If the decoration changes
  211. over time then the function needs to also handle the cleanup.
  212. */
  213. type FilenameIterator interface {
  214. /*
  215. Basename decorades the initial file name.
  216. */
  217. Basename(filename string) string
  218. /*
  219. NextName returns the next file name based on the current file name.
  220. An empty string means the end of the iteration.
  221. */
  222. NextName(currentName string) string
  223. }
  224. /*
  225. ConsecutiveNumberIterator creates a new file name iterator which adds numbers
  226. at the end of files. Up to maxNum files will be created. A maxNum parameter
  227. < 1 means there is no limit.
  228. */
  229. func ConsecutiveNumberIterator(maxNum int) FilenameIterator {
  230. return &consecutiveNumberIterator{maxNum}
  231. }
  232. /*
  233. consecutiveNumberIterator is the implementation of the consecutive number
  234. file iterator.
  235. */
  236. type consecutiveNumberIterator struct {
  237. maxNum int
  238. }
  239. /*
  240. Basename decorades the initial file name.
  241. */
  242. func (it *consecutiveNumberIterator) Basename(filename string) string {
  243. return filename
  244. }
  245. /*
  246. NextName returns the next file name based on the current file name.
  247. An empty string means the end of the iteration.
  248. */
  249. func (it *consecutiveNumberIterator) NextName(currentName string) string {
  250. if i := strings.LastIndex(currentName, "."); i > 0 {
  251. if num, err := strconv.ParseInt(currentName[i+1:], 10, 64); err == nil {
  252. nextNum := int(num + 1)
  253. if it.maxNum > 0 && nextNum > it.maxNum {
  254. return ""
  255. }
  256. return fmt.Sprintf("%s.%v", currentName[:i], nextNum)
  257. }
  258. }
  259. return fmt.Sprintf("%s.1", currentName)
  260. }
  261. /*
  262. DailyDateIterator creates a new file name iterator which adds dates at the
  263. end of files. The log will be switched at least once every day. Up to maxNumPerDay
  264. files will be created per day. A maxNumPerDay parameter < 1 means there is no limit.
  265. Up to maxDays different days will be kept (oldest ones are deleted). A maxDays
  266. parameter < 1 means everything is kept.
  267. */
  268. func DailyDateIterator(maxNumPerDay int, maxDays int) FilenameIterator {
  269. return &dailyDateIterator{&consecutiveNumberIterator{maxNumPerDay}, maxDays, timeutil.MakeTimestamp}
  270. }
  271. /*
  272. consecutiveNumberIterator is the implementation of the consecutive number
  273. file iterator.
  274. */
  275. type dailyDateIterator struct {
  276. *consecutiveNumberIterator
  277. maxDays int
  278. tsFunc func() string // Timestamp function
  279. }
  280. /*
  281. NextName returns the next file name based on the current file name.
  282. An empty string means the end of the iteration.
  283. */
  284. func (it *dailyDateIterator) Basename(filename string) string {
  285. // Get todays date
  286. ts := it.tsFunc()
  287. today, _ := timeutil.TimestampString(ts, "UTC")
  288. today = today[:10]
  289. // Cleanup old files
  290. if it.maxDays > 0 {
  291. prefix := path.Base(filename)
  292. dir := path.Dir(filename)
  293. if files, err := ioutil.ReadDir(dir); err == nil {
  294. var datesToConsider []string
  295. // Collect all relevant files
  296. foundToday := false
  297. for _, f := range files {
  298. if strings.HasPrefix(f.Name(), prefix) && len(f.Name()) > len(prefix) {
  299. dateString := f.Name()[len(prefix)+1:]
  300. if !strings.ContainsRune(dateString, '.') {
  301. datesToConsider = append(datesToConsider, dateString)
  302. if !foundToday {
  303. foundToday = dateString == today
  304. }
  305. }
  306. }
  307. }
  308. // Make sure today is one of the dates
  309. if !foundToday {
  310. datesToConsider = append(datesToConsider, today)
  311. }
  312. // Sort them so the newest ones are kept
  313. sort.Strings(datesToConsider)
  314. // Check if files need to be removed
  315. if len(datesToConsider) > it.maxDays {
  316. datesToRemove := datesToConsider[:len(datesToConsider)-it.maxDays]
  317. for _, f := range files {
  318. for _, dateToRemove := range datesToRemove {
  319. if strings.HasPrefix(f.Name(), fmt.Sprintf("%s.%s", prefix, dateToRemove)) {
  320. os.Remove(path.Join(dir, f.Name()))
  321. }
  322. }
  323. }
  324. }
  325. }
  326. }
  327. return fmt.Sprintf("%s.%s", filename, today)
  328. }