updater.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package lifecycle
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log/slog"
  10. "mime"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "path"
  15. "path/filepath"
  16. "runtime"
  17. "strings"
  18. "time"
  19. "github.com/jmorganca/ollama/auth"
  20. "github.com/jmorganca/ollama/version"
  21. )
  22. var (
  23. UpdateCheckURLBase = "https://ollama.com/api/update"
  24. UpdateDownloaded = false
  25. )
  26. // TODO - maybe move up to the API package?
  27. type UpdateResponse struct {
  28. UpdateURL string `json:"url"`
  29. UpdateVersion string `json:"version"`
  30. }
  31. func getClient(req *http.Request) http.Client {
  32. proxyURL, err := http.ProxyFromEnvironment(req)
  33. if err != nil {
  34. slog.Warn(fmt.Sprintf("failed to handle proxy: %s", err))
  35. return http.Client{}
  36. }
  37. return http.Client{
  38. Transport: &http.Transport{
  39. Proxy: http.ProxyURL(proxyURL),
  40. },
  41. }
  42. }
  43. func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) {
  44. var updateResp UpdateResponse
  45. requestURL, err := url.Parse(UpdateCheckURLBase)
  46. if err != nil {
  47. return false, updateResp
  48. }
  49. query := requestURL.Query()
  50. query.Add("os", runtime.GOOS)
  51. query.Add("arch", runtime.GOARCH)
  52. query.Add("version", version.Version)
  53. query.Add("ts", fmt.Sprintf("%d", time.Now().Unix()))
  54. nonce, err := auth.NewNonce(rand.Reader, 16)
  55. if err != nil {
  56. return false, updateResp
  57. }
  58. query.Add("nonce", nonce)
  59. requestURL.RawQuery = query.Encode()
  60. data := []byte(fmt.Sprintf("%s,%s", http.MethodGet, requestURL.RequestURI()))
  61. signature, err := auth.Sign(ctx, data)
  62. if err != nil {
  63. return false, updateResp
  64. }
  65. req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
  66. if err != nil {
  67. slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
  68. return false, updateResp
  69. }
  70. req.Header.Set("Authorization", signature)
  71. req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
  72. client := getClient(req)
  73. slog.Debug(fmt.Sprintf("checking for available update at %s with headers %v", requestURL, req.Header))
  74. resp, err := client.Do(req)
  75. if err != nil {
  76. slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
  77. return false, updateResp
  78. }
  79. defer resp.Body.Close()
  80. if resp.StatusCode == 204 {
  81. slog.Debug("check update response 204 (current version is up to date)")
  82. return false, updateResp
  83. }
  84. body, err := io.ReadAll(resp.Body)
  85. if err != nil {
  86. slog.Warn(fmt.Sprintf("failed to read body response: %s", err))
  87. }
  88. err = json.Unmarshal(body, &updateResp)
  89. if err != nil {
  90. slog.Warn(fmt.Sprintf("malformed response checking for update: %s", err))
  91. return false, updateResp
  92. }
  93. // Extract the version string from the URL in the github release artifact path
  94. updateResp.UpdateVersion = path.Base(path.Dir(updateResp.UpdateURL))
  95. slog.Info("New update available at " + updateResp.UpdateURL)
  96. return true, updateResp
  97. }
  98. // Returns true if we downloaded a new update, false if we already had it
  99. func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error {
  100. // Do a head first to check etag info
  101. req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil)
  102. if err != nil {
  103. return err
  104. }
  105. client := getClient(req)
  106. resp, err := client.Do(req)
  107. if err != nil {
  108. return fmt.Errorf("error checking update: %w", err)
  109. }
  110. if resp.StatusCode != 200 {
  111. return fmt.Errorf("unexpected status attempting to download update %d", resp.StatusCode)
  112. }
  113. resp.Body.Close()
  114. etag := strings.Trim(resp.Header.Get("etag"), "\"")
  115. if etag == "" {
  116. slog.Debug("no etag detected, falling back to filename based dedup")
  117. etag = "_"
  118. }
  119. filename := Installer
  120. _, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition"))
  121. if err == nil {
  122. filename = params["filename"]
  123. }
  124. stageFilename := filepath.Join(UpdateStageDir, etag, filename)
  125. // Check to see if we already have it downloaded
  126. _, err = os.Stat(stageFilename)
  127. if err == nil {
  128. slog.Debug("update already downloaded")
  129. return nil
  130. }
  131. cleanupOldDownloads()
  132. req.Method = http.MethodGet
  133. resp, err = client.Do(req)
  134. if err != nil {
  135. return fmt.Errorf("error checking update: %w", err)
  136. }
  137. defer resp.Body.Close()
  138. etag = strings.Trim(resp.Header.Get("etag"), "\"")
  139. if etag == "" {
  140. slog.Debug("no etag detected, falling back to filename based dedup") // TODO probably can get rid of this redundant log
  141. etag = "_"
  142. }
  143. stageFilename = filepath.Join(UpdateStageDir, etag, filename)
  144. _, err = os.Stat(filepath.Dir(stageFilename))
  145. if errors.Is(err, os.ErrNotExist) {
  146. if err := os.MkdirAll(filepath.Dir(stageFilename), 0o755); err != nil {
  147. return fmt.Errorf("create ollama dir %s: %v", filepath.Dir(stageFilename), err)
  148. }
  149. }
  150. payload, err := io.ReadAll(resp.Body)
  151. if err != nil {
  152. return fmt.Errorf("failed to read body response: %w", err)
  153. }
  154. fp, err := os.OpenFile(stageFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
  155. if err != nil {
  156. return fmt.Errorf("write payload %s: %w", stageFilename, err)
  157. }
  158. defer fp.Close()
  159. if n, err := fp.Write(payload); err != nil || n != len(payload) {
  160. return fmt.Errorf("write payload %s: %d vs %d -- %w", stageFilename, n, len(payload), err)
  161. }
  162. slog.Info("new update downloaded " + stageFilename)
  163. UpdateDownloaded = true
  164. return nil
  165. }
  166. func cleanupOldDownloads() {
  167. files, err := os.ReadDir(UpdateStageDir)
  168. if err != nil && errors.Is(err, os.ErrNotExist) {
  169. // Expected behavior on first run
  170. return
  171. } else if err != nil {
  172. slog.Warn(fmt.Sprintf("failed to list stage dir: %s", err))
  173. return
  174. }
  175. for _, file := range files {
  176. fullname := filepath.Join(UpdateStageDir, file.Name())
  177. slog.Debug("cleaning up old download: " + fullname)
  178. err = os.RemoveAll(fullname)
  179. if err != nil {
  180. slog.Warn(fmt.Sprintf("failed to cleanup stale update download %s", err))
  181. }
  182. }
  183. }
  184. func StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) {
  185. go func() {
  186. // Don't blast an update message immediately after startup
  187. // time.Sleep(30 * time.Second)
  188. time.Sleep(3 * time.Second)
  189. for {
  190. available, resp := IsNewReleaseAvailable(ctx)
  191. if available {
  192. err := DownloadNewRelease(ctx, resp)
  193. if err != nil {
  194. slog.Error(fmt.Sprintf("failed to download new release: %s", err))
  195. }
  196. err = cb(resp.UpdateVersion)
  197. if err != nil {
  198. slog.Warn(fmt.Sprintf("failed to register update available with tray: %s", err))
  199. }
  200. }
  201. select {
  202. case <-ctx.Done():
  203. slog.Debug("stopping background update checker")
  204. return
  205. default:
  206. time.Sleep(60 * 60 * time.Second)
  207. }
  208. }
  209. }()
  210. }