You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lfs.go 15KB

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