download.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package geolite
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "os"
  7. "path/filepath"
  8. "strconv"
  9. "github.com/ulikunitz/xz"
  10. "github.com/uozi-tech/cosy"
  11. "github.com/uozi-tech/cosy/settings"
  12. )
  13. const (
  14. DownloadURL = "http://cloud.nginxui.com/geolite/GeoLite2-City.mmdb.xz"
  15. )
  16. type DownloadProgressWriter struct {
  17. io.Writer
  18. totalSize int64
  19. currentSize int64
  20. progressChan chan<- float64
  21. lastReported float64
  22. reportInterval float64 // Report only when progress changes by this amount
  23. }
  24. func (pw *DownloadProgressWriter) Write(p []byte) (int, error) {
  25. n, err := pw.Writer.Write(p)
  26. pw.currentSize += int64(n)
  27. progress := float64(pw.currentSize) / float64(pw.totalSize) * 100
  28. // Debounce: only send updates when progress changes by reportInterval or reaches 100%
  29. if progress-pw.lastReported >= pw.reportInterval || progress >= 100 {
  30. select {
  31. case pw.progressChan <- progress:
  32. pw.lastReported = progress
  33. default:
  34. }
  35. }
  36. return n, err
  37. }
  38. // GetDBPath returns the path to the GeoLite2 database file
  39. func GetDBPath() string {
  40. confDir := filepath.Dir(settings.ConfPath)
  41. return filepath.Join(confDir, "GeoLite2-City.mmdb")
  42. }
  43. // GetDBXZPath returns the path to the compressed GeoLite2 database file
  44. func GetDBXZPath() string {
  45. confDir := filepath.Dir(settings.ConfPath)
  46. return filepath.Join(confDir, "GeoLite2-City.mmdb.xz")
  47. }
  48. // DownloadGeoLiteDB downloads the GeoLite2 database
  49. func DownloadGeoLiteDB(progressChan chan float64) error {
  50. client := &http.Client{}
  51. req, err := http.NewRequest("GET", DownloadURL, nil)
  52. if err != nil {
  53. return cosy.WrapErrorWithParams(ErrDownloadFailed, err.Error())
  54. }
  55. resp, err := client.Do(req)
  56. if err != nil {
  57. return cosy.WrapErrorWithParams(ErrDownloadFailed, err.Error())
  58. }
  59. defer resp.Body.Close()
  60. if resp.StatusCode != http.StatusOK {
  61. return cosy.WrapErrorWithParams(ErrDownloadFailed, fmt.Sprintf("status code: %d", resp.StatusCode))
  62. }
  63. totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
  64. if err != nil {
  65. return cosy.WrapErrorWithParams(ErrFailedToGetFileSize, err.Error())
  66. }
  67. xzPath := GetDBXZPath()
  68. file, err := os.Create(xzPath)
  69. if err != nil {
  70. return cosy.WrapErrorWithParams(ErrFailedToCreateFile, err.Error())
  71. }
  72. defer file.Close()
  73. progressWriter := &DownloadProgressWriter{
  74. Writer: file,
  75. totalSize: totalSize,
  76. progressChan: progressChan,
  77. reportInterval: 1.0, // Report every 1% change
  78. }
  79. _, err = io.Copy(progressWriter, resp.Body)
  80. if err != nil {
  81. os.Remove(xzPath) // Clean up on error
  82. return cosy.WrapErrorWithParams(ErrFailedToSaveFile, err.Error())
  83. }
  84. return nil
  85. }
  86. // DecompressGeoLiteDB decompresses the .xz file to .mmdb
  87. func DecompressGeoLiteDB(progressChan chan float64) error {
  88. xzPath := GetDBXZPath()
  89. dbPath := GetDBPath()
  90. // Open compressed file
  91. xzFile, err := os.Open(xzPath)
  92. if err != nil {
  93. return cosy.WrapErrorWithParams(ErrFailedToOpenFile, err.Error())
  94. }
  95. defer xzFile.Close()
  96. // Get compressed file size
  97. fileInfo, err := xzFile.Stat()
  98. if err != nil {
  99. return cosy.WrapErrorWithParams(ErrFailedToGetFileSize, err.Error())
  100. }
  101. compressedSize := fileInfo.Size()
  102. // Create XZ reader
  103. xzReader, err := xz.NewReader(xzFile)
  104. if err != nil {
  105. return cosy.WrapErrorWithParams(ErrFailedToCreateXZReader, err.Error())
  106. }
  107. // Create output file
  108. outFile, err := os.Create(dbPath)
  109. if err != nil {
  110. return cosy.WrapErrorWithParams(ErrFailedToCreateFile, err.Error())
  111. }
  112. defer outFile.Close()
  113. // Decompress with progress tracking
  114. buf := make([]byte, 64*1024) // 64KB buffer for better performance
  115. var decompressedSize int64
  116. var lastReportedProgress float64
  117. const reportInterval = 2.0 // Report every 2% change
  118. // Estimate: XZ typically compresses to 10-20% of original size
  119. // We'll use 15% (compression ratio ~6.67) as middle estimate
  120. const estimatedCompressionRatio = 6.67
  121. estimatedTotalSize := float64(compressedSize) * estimatedCompressionRatio
  122. for {
  123. n, readErr := xzReader.Read(buf)
  124. if n > 0 {
  125. if _, writeErr := outFile.Write(buf[:n]); writeErr != nil {
  126. os.Remove(dbPath) // Clean up on error
  127. return cosy.WrapErrorWithParams(ErrFailedToWriteData, writeErr.Error())
  128. }
  129. decompressedSize += int64(n)
  130. // Calculate progress based on estimated total size
  131. progress := (float64(decompressedSize) / estimatedTotalSize) * 100
  132. if progress > 99 {
  133. progress = 99 // Cap at 99% until actually complete
  134. }
  135. // Debounce: only send updates when progress changes significantly
  136. if progress-lastReportedProgress >= reportInterval || readErr == io.EOF {
  137. select {
  138. case progressChan <- progress:
  139. lastReportedProgress = progress
  140. default:
  141. }
  142. }
  143. }
  144. if readErr == io.EOF {
  145. // Send 100% on completion
  146. select {
  147. case progressChan <- 100:
  148. default:
  149. }
  150. break
  151. }
  152. if readErr != nil {
  153. os.Remove(dbPath) // Clean up on error
  154. return cosy.WrapErrorWithParams(ErrFailedToReadData, readErr.Error())
  155. }
  156. }
  157. // Delete the .xz file after successful decompression
  158. if err := os.Remove(xzPath); err != nil {
  159. // Log but don't fail if we can't delete the compressed file
  160. return cosy.WrapErrorWithParams(ErrFailedToDeleteCompressed, err.Error())
  161. }
  162. return nil
  163. }
  164. // DBExists checks if the GeoLite2 database file exists
  165. func DBExists() bool {
  166. _, err := os.Stat(GetDBPath())
  167. return err == nil
  168. }