gitea源码

lfs.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package setting
  4. import (
  5. "bytes"
  6. "fmt"
  7. gotemplate "html/template"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "path"
  12. "strconv"
  13. "strings"
  14. git_model "code.gitea.io/gitea/models/git"
  15. "code.gitea.io/gitea/modules/charset"
  16. "code.gitea.io/gitea/modules/container"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/git/attribute"
  19. "code.gitea.io/gitea/modules/git/pipeline"
  20. "code.gitea.io/gitea/modules/lfs"
  21. "code.gitea.io/gitea/modules/log"
  22. repo_module "code.gitea.io/gitea/modules/repository"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/storage"
  25. "code.gitea.io/gitea/modules/templates"
  26. "code.gitea.io/gitea/modules/typesniffer"
  27. "code.gitea.io/gitea/modules/util"
  28. "code.gitea.io/gitea/services/context"
  29. )
  30. const (
  31. tplSettingsLFS templates.TplName = "repo/settings/lfs"
  32. tplSettingsLFSLocks templates.TplName = "repo/settings/lfs_locks"
  33. tplSettingsLFSFile templates.TplName = "repo/settings/lfs_file"
  34. tplSettingsLFSFileFind templates.TplName = "repo/settings/lfs_file_find"
  35. tplSettingsLFSPointers templates.TplName = "repo/settings/lfs_pointers"
  36. )
  37. // LFSFiles shows a repository's LFS files
  38. func LFSFiles(ctx *context.Context) {
  39. if !setting.LFS.StartServer {
  40. ctx.NotFound(nil)
  41. return
  42. }
  43. page := max(ctx.FormInt("page"), 1)
  44. total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID)
  45. if err != nil {
  46. ctx.ServerError("LFSFiles", err)
  47. return
  48. }
  49. ctx.Data["Total"] = total
  50. pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
  51. ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
  52. ctx.Data["PageIsSettingsLFS"] = true
  53. lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
  54. if err != nil {
  55. ctx.ServerError("LFSFiles", err)
  56. return
  57. }
  58. ctx.Data["LFSFiles"] = lfsMetaObjects
  59. ctx.Data["Page"] = pager
  60. ctx.HTML(http.StatusOK, tplSettingsLFS)
  61. }
  62. // LFSLocks shows a repository's LFS locks
  63. func LFSLocks(ctx *context.Context) {
  64. if !setting.LFS.StartServer {
  65. ctx.NotFound(nil)
  66. return
  67. }
  68. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  69. page := max(ctx.FormInt("page"), 1)
  70. total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID)
  71. if err != nil {
  72. ctx.ServerError("LFSLocks", err)
  73. return
  74. }
  75. ctx.Data["Total"] = total
  76. pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
  77. ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
  78. ctx.Data["PageIsSettingsLFS"] = true
  79. lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
  80. if err != nil {
  81. ctx.ServerError("LFSLocks", err)
  82. return
  83. }
  84. if err := lfsLocks.LoadAttributes(ctx); err != nil {
  85. ctx.ServerError("LFSLocks", err)
  86. return
  87. }
  88. ctx.Data["LFSLocks"] = lfsLocks
  89. if len(lfsLocks) == 0 {
  90. ctx.Data["Page"] = pager
  91. ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
  92. return
  93. }
  94. // Clone base repo.
  95. tmpBasePath, cleanup, err := repo_module.CreateTemporaryPath("locks")
  96. if err != nil {
  97. log.Error("Failed to create temporary path: %v", err)
  98. ctx.ServerError("LFSLocks", err)
  99. return
  100. }
  101. defer cleanup()
  102. if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
  103. Bare: true,
  104. Shared: true,
  105. }); err != nil {
  106. log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
  107. ctx.ServerError("LFSLocks", fmt.Errorf("failed to clone repository: %s (%w)", ctx.Repo.Repository.FullName(), err))
  108. return
  109. }
  110. gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
  111. if err != nil {
  112. log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
  113. ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %w", tmpBasePath, err))
  114. return
  115. }
  116. defer gitRepo.Close()
  117. checker, err := attribute.NewBatchChecker(gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable})
  118. if err != nil {
  119. log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
  120. ctx.ServerError("LFSLocks", err)
  121. return
  122. }
  123. defer checker.Close()
  124. lockables := make([]bool, len(lfsLocks))
  125. filenames := make([]string, len(lfsLocks))
  126. for i, lock := range lfsLocks {
  127. filenames[i] = lock.Path
  128. attrs, err := checker.CheckPath(lock.Path)
  129. if err != nil {
  130. log.Error("Unable to check attributes in %s: %s (%v)", tmpBasePath, lock.Path, err)
  131. continue
  132. }
  133. lockables[i] = attrs.Get(attribute.Lockable).ToBool().Value()
  134. }
  135. ctx.Data["Lockables"] = lockables
  136. filelist, err := gitRepo.LsFiles(filenames...)
  137. if err != nil {
  138. log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
  139. ctx.ServerError("LFSLocks", err)
  140. return
  141. }
  142. fileset := make(container.Set[string], len(filelist))
  143. fileset.AddMultiple(filelist...)
  144. linkable := make([]bool, len(lfsLocks))
  145. for i, lock := range lfsLocks {
  146. linkable[i] = fileset.Contains(lock.Path)
  147. }
  148. ctx.Data["Linkable"] = linkable
  149. ctx.Data["Page"] = pager
  150. ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
  151. }
  152. // LFSLockFile locks a file
  153. func LFSLockFile(ctx *context.Context) {
  154. if !setting.LFS.StartServer {
  155. ctx.NotFound(nil)
  156. return
  157. }
  158. originalPath := ctx.FormString("path")
  159. lockPath := originalPath
  160. if len(lockPath) == 0 {
  161. ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
  162. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  163. return
  164. }
  165. if lockPath[len(lockPath)-1] == '/' {
  166. ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
  167. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  168. return
  169. }
  170. lockPath = util.PathJoinRel(lockPath)
  171. if len(lockPath) == 0 {
  172. ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
  173. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  174. return
  175. }
  176. _, err := git_model.CreateLFSLock(ctx, ctx.Repo.Repository, &git_model.LFSLock{
  177. Path: lockPath,
  178. OwnerID: ctx.Doer.ID,
  179. })
  180. if err != nil {
  181. if git_model.IsErrLFSLockAlreadyExist(err) {
  182. ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
  183. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  184. return
  185. }
  186. ctx.ServerError("LFSLockFile", err)
  187. return
  188. }
  189. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  190. }
  191. // LFSUnlock forcibly unlocks an LFS lock
  192. func LFSUnlock(ctx *context.Context) {
  193. if !setting.LFS.StartServer {
  194. ctx.NotFound(nil)
  195. return
  196. }
  197. _, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), ctx.Repo.Repository, ctx.Doer, true)
  198. if err != nil {
  199. ctx.ServerError("LFSUnlock", err)
  200. return
  201. }
  202. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
  203. }
  204. // LFSFileGet serves a single LFS file
  205. func LFSFileGet(ctx *context.Context) {
  206. if !setting.LFS.StartServer {
  207. ctx.NotFound(nil)
  208. return
  209. }
  210. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  211. oid := ctx.PathParam("oid")
  212. p := lfs.Pointer{Oid: oid}
  213. if !p.IsValid() {
  214. ctx.NotFound(nil)
  215. return
  216. }
  217. ctx.Data["Title"] = oid
  218. ctx.Data["PageIsSettingsLFS"] = true
  219. meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
  220. if err != nil {
  221. if err == git_model.ErrLFSObjectNotExist {
  222. ctx.NotFound(nil)
  223. return
  224. }
  225. ctx.ServerError("LFSFileGet", err)
  226. return
  227. }
  228. ctx.Data["LFSFile"] = meta
  229. dataRc, err := lfs.ReadMetaObject(meta.Pointer)
  230. if err != nil {
  231. ctx.ServerError("LFSFileGet", err)
  232. return
  233. }
  234. defer dataRc.Close()
  235. buf := make([]byte, 1024)
  236. n, err := util.ReadAtMost(dataRc, buf)
  237. if err != nil {
  238. ctx.ServerError("Data", err)
  239. return
  240. }
  241. buf = buf[:n]
  242. st := typesniffer.DetectContentType(buf)
  243. // FIXME: there is no IsPlainText set, but template uses it
  244. ctx.Data["IsTextFile"] = st.IsText()
  245. ctx.Data["FileSize"] = meta.Size
  246. // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct"
  247. ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
  248. switch {
  249. case st.IsRepresentableAsText():
  250. if meta.Size >= setting.UI.MaxDisplayFileSize {
  251. ctx.Data["IsFileTooLarge"] = true
  252. break
  253. }
  254. if st.IsSvgImage() {
  255. ctx.Data["IsImageFile"] = true
  256. }
  257. rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
  258. // Building code view blocks with line number on server side.
  259. // FIXME: the logic is not right here: it first calls EscapeControlReader then calls HTMLEscapeString: double-escaping
  260. escapedContent := &bytes.Buffer{}
  261. ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale)
  262. var output bytes.Buffer
  263. lines := strings.Split(escapedContent.String(), "\n")
  264. // Remove blank line at the end of file
  265. if len(lines) > 0 && lines[len(lines)-1] == "" {
  266. lines = lines[:len(lines)-1]
  267. }
  268. for index, line := range lines {
  269. line = gotemplate.HTMLEscapeString(line)
  270. if index != len(lines)-1 {
  271. line += "\n"
  272. }
  273. output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
  274. }
  275. ctx.Data["FileContent"] = gotemplate.HTML(output.String())
  276. output.Reset()
  277. for i := 0; i < len(lines); i++ {
  278. output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
  279. }
  280. ctx.Data["LineNums"] = gotemplate.HTML(output.String())
  281. case st.IsVideo():
  282. ctx.Data["IsVideoFile"] = true
  283. case st.IsAudio():
  284. ctx.Data["IsAudioFile"] = true
  285. case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
  286. ctx.Data["IsImageFile"] = true
  287. default:
  288. // TODO: the logic is not the same as "renderFile" in "view.go"
  289. }
  290. ctx.HTML(http.StatusOK, tplSettingsLFSFile)
  291. }
  292. // LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
  293. func LFSDelete(ctx *context.Context) {
  294. if !setting.LFS.StartServer {
  295. ctx.NotFound(nil)
  296. return
  297. }
  298. oid := ctx.PathParam("oid")
  299. p := lfs.Pointer{Oid: oid}
  300. if !p.IsValid() {
  301. ctx.NotFound(nil)
  302. return
  303. }
  304. count, err := git_model.RemoveLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
  305. if err != nil {
  306. ctx.ServerError("LFSDelete", err)
  307. return
  308. }
  309. // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
  310. // Please note a similar condition happens in models/repo.go DeleteRepository
  311. if count == 0 {
  312. oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
  313. err = storage.LFS.Delete(oidPath)
  314. if err != nil {
  315. ctx.ServerError("LFSDelete", err)
  316. return
  317. }
  318. }
  319. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
  320. }
  321. // LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
  322. func LFSFileFind(ctx *context.Context) {
  323. if !setting.LFS.StartServer {
  324. ctx.NotFound(nil)
  325. return
  326. }
  327. oid := ctx.FormString("oid")
  328. size := ctx.FormInt64("size")
  329. if len(oid) == 0 || size == 0 {
  330. ctx.NotFound(nil)
  331. return
  332. }
  333. sha := ctx.FormString("sha")
  334. ctx.Data["Title"] = oid
  335. ctx.Data["PageIsSettingsLFS"] = true
  336. objectFormat := ctx.Repo.GetObjectFormat()
  337. var objectID git.ObjectID
  338. if len(sha) == 0 {
  339. pointer := lfs.Pointer{Oid: oid, Size: size}
  340. objectID = git.ComputeBlobHash(objectFormat, []byte(pointer.StringContent()))
  341. sha = objectID.String()
  342. } else {
  343. objectID = git.MustIDFromString(sha)
  344. }
  345. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  346. ctx.Data["Oid"] = oid
  347. ctx.Data["Size"] = size
  348. ctx.Data["SHA"] = sha
  349. results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, objectID)
  350. if err != nil && err != io.EOF {
  351. log.Error("Failure in FindLFSFile: %v", err)
  352. ctx.ServerError("LFSFind: FindLFSFile.", err)
  353. return
  354. }
  355. ctx.Data["Results"] = results
  356. ctx.HTML(http.StatusOK, tplSettingsLFSFileFind)
  357. }
  358. // LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
  359. func LFSPointerFiles(ctx *context.Context) {
  360. if !setting.LFS.StartServer {
  361. ctx.NotFound(nil)
  362. return
  363. }
  364. ctx.Data["PageIsSettingsLFS"] = true
  365. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  366. var err error
  367. err = func() error {
  368. pointerChan := make(chan lfs.PointerBlob)
  369. errChan := make(chan error, 1)
  370. go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
  371. numPointers := 0
  372. var numAssociated, numNoExist, numAssociatable int
  373. type pointerResult struct {
  374. SHA string
  375. Oid string
  376. Size int64
  377. InRepo bool
  378. Exists bool
  379. Accessible bool
  380. Associatable bool
  381. }
  382. results := []pointerResult{}
  383. contentStore := lfs.NewContentStore()
  384. repo := ctx.Repo.Repository
  385. for pointerBlob := range pointerChan {
  386. numPointers++
  387. result := pointerResult{
  388. SHA: pointerBlob.Hash,
  389. Oid: pointerBlob.Oid,
  390. Size: pointerBlob.Size,
  391. }
  392. if _, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid); err != nil {
  393. if err != git_model.ErrLFSObjectNotExist {
  394. return err
  395. }
  396. } else {
  397. result.InRepo = true
  398. }
  399. result.Exists, err = contentStore.Exists(pointerBlob.Pointer)
  400. if err != nil {
  401. return err
  402. }
  403. if result.Exists {
  404. if !result.InRepo {
  405. // Can we fix?
  406. // OK well that's "simple"
  407. // - we need to check whether current user has access to a repo that has access to the file
  408. result.Associatable, err = git_model.LFSObjectAccessible(ctx, ctx.Doer, pointerBlob.Oid)
  409. if err != nil {
  410. return err
  411. }
  412. if !result.Associatable {
  413. associated, err := git_model.ExistsLFSObject(ctx, pointerBlob.Oid)
  414. if err != nil {
  415. return err
  416. }
  417. result.Associatable = !associated
  418. }
  419. }
  420. }
  421. result.Accessible = result.InRepo || result.Associatable
  422. if result.InRepo {
  423. numAssociated++
  424. }
  425. if !result.Exists {
  426. numNoExist++
  427. }
  428. if result.Associatable {
  429. numAssociatable++
  430. }
  431. results = append(results, result)
  432. }
  433. err, has := <-errChan
  434. if has {
  435. return err
  436. }
  437. ctx.Data["Pointers"] = results
  438. ctx.Data["NumPointers"] = numPointers
  439. ctx.Data["NumAssociated"] = numAssociated
  440. ctx.Data["NumAssociatable"] = numAssociatable
  441. ctx.Data["NumNoExist"] = numNoExist
  442. ctx.Data["NumNotAssociated"] = numPointers - numAssociated
  443. return nil
  444. }()
  445. if err != nil {
  446. ctx.ServerError("LFSPointerFiles", err)
  447. return
  448. }
  449. ctx.HTML(http.StatusOK, tplSettingsLFSPointers)
  450. }
  451. // LFSAutoAssociate auto associates accessible lfs files
  452. func LFSAutoAssociate(ctx *context.Context) {
  453. if !setting.LFS.StartServer {
  454. ctx.NotFound(nil)
  455. return
  456. }
  457. oids := ctx.FormStrings("oid")
  458. metas := make([]*git_model.LFSMetaObject, len(oids))
  459. for i, oid := range oids {
  460. idx := strings.IndexRune(oid, ' ')
  461. if idx < 0 || idx+1 > len(oid) {
  462. ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s", oid))
  463. return
  464. }
  465. var err error
  466. metas[i] = &git_model.LFSMetaObject{}
  467. metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
  468. if err != nil {
  469. ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s %w", oid, err))
  470. return
  471. }
  472. metas[i].Oid = oid[:idx]
  473. // metas[i].RepositoryID = ctx.Repo.Repository.ID
  474. }
  475. if err := git_model.LFSAutoAssociate(ctx, metas, ctx.Doer, ctx.Repo.Repository.ID); err != nil {
  476. ctx.ServerError("LFSAutoAssociate", err)
  477. return
  478. }
  479. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
  480. }