Просмотр исходного кода

Merge pull request #22178 from overleaf/jpa-file-view-hash-2

[web] migrate file-view to download from history-v1 (via web) 2/2

GitOrigin-RevId: 93172cbbd2d19a55bb27f5b0ca0b494f815a3632
Jakob Ackermann 1 год назад
Родитель
Сommit
faff4f1d7e

+ 2 - 7
services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx

@@ -33,7 +33,6 @@ import {
 } from '../errors'
 } from '../errors'
 import { Folder } from '../../../../../types/folder'
 import { Folder } from '../../../../../types/folder'
 import { useReferencesContext } from '@/features/ide-react/context/references-context'
 import { useReferencesContext } from '@/features/ide-react/context/references-context'
-import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 
 type DroppedFile = File & {
 type DroppedFile = File & {
   relativePath?: string
   relativePath?: string
@@ -221,7 +220,6 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
   const { fileTreeReadOnly } = useFileTreeData()
   const { fileTreeReadOnly } = useFileTreeData()
   const { indexAllReferences } = useReferencesContext()
   const { indexAllReferences } = useReferencesContext()
-  const { fileTreeFromHistory } = useSnapshotContext()
 
 
   const [state, dispatch] = useReducer(
   const [state, dispatch] = useReducer(
     fileTreeReadOnly
     fileTreeReadOnly
@@ -494,17 +492,14 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
       const selectedEntity = findInTree(fileTreeData, selectedEntityId)
       const selectedEntity = findInTree(fileTreeData, selectedEntityId)
 
 
       if (selectedEntity?.type === 'fileRef') {
       if (selectedEntity?.type === 'fileRef') {
-        if (fileTreeFromHistory) {
-          return `/project/${projectId}/blob/${selectedEntity.entity.hash}`
-        }
-        return `/project/${projectId}/file/${selectedEntityId}`
+        return `/project/${projectId}/blob/${selectedEntity.entity.hash}?fallback=${selectedEntityId}`
       }
       }
 
 
       if (selectedEntity?.type === 'doc') {
       if (selectedEntity?.type === 'doc') {
         return `/project/${projectId}/doc/${selectedEntityId}/download`
         return `/project/${projectId}/doc/${selectedEntityId}/download`
       }
       }
     }
     }
-  }, [fileTreeData, projectId, selectedEntityIds, fileTreeFromHistory])
+  }, [fileTreeData, projectId, selectedEntityIds])
 
 
   // TODO: wrap in useMemo
   // TODO: wrap in useMemo
   const value = {
   const value = {

+ 2 - 5
services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx

@@ -10,7 +10,6 @@ import {
   pathInFolder,
   pathInFolder,
 } from '@/features/file-tree/util/path'
 } from '@/features/file-tree/util/path'
 import { PreviewPath } from '../../../../../types/preview-path'
 import { PreviewPath } from '../../../../../types/preview-path'
-import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 
 type FileTreePathContextValue = {
 type FileTreePathContextValue = {
   pathInFolder: (id: string) => string | null
   pathInFolder: (id: string) => string | null
@@ -25,7 +24,6 @@ export const FileTreePathContext = createContext<
 
 
 export const FileTreePathProvider: FC = ({ children }) => {
 export const FileTreePathProvider: FC = ({ children }) => {
   const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
   const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
-  const { fileTreeFromHistory } = useSnapshotContext()
   const projectId = getMeta('ol-project_id')
   const projectId = getMeta('ol-project_id')
 
 
   const pathInFileTree = useCallback(
   const pathInFileTree = useCallback(
@@ -39,9 +37,8 @@ export const FileTreePathProvider: FC = ({ children }) => {
   )
   )
 
 
   const previewByPathInFileTree = useCallback(
   const previewByPathInFileTree = useCallback(
-    (path: string) =>
-      previewByPath(fileTreeData, projectId, path, fileTreeFromHistory),
-    [fileTreeData, projectId, fileTreeFromHistory]
+    (path: string) => previewByPath(fileTreeData, projectId, path),
+    [fileTreeData, projectId]
   )
   )
 
 
   const dirnameInFileTree = useCallback(
   const dirnameInFileTree = useCallback(

+ 2 - 5
services/web/frontend/js/features/file-tree/util/path.ts

@@ -106,8 +106,7 @@ export function findEntityByPath(
 export function previewByPath(
 export function previewByPath(
   folder: Folder,
   folder: Folder,
   projectId: string,
   projectId: string,
-  path: string,
-  fileTreeFromHistory: boolean
+  path: string
 ): PreviewPath | null {
 ): PreviewPath | null {
   for (const suffix of [
   for (const suffix of [
     '',
     '',
@@ -125,9 +124,7 @@ export function previewByPath(
     if (result?.type === 'fileRef') {
     if (result?.type === 'fileRef') {
       const { name, _id: id, hash } = result.entity
       const { name, _id: id, hash } = result.entity
       return {
       return {
-        url: fileTreeFromHistory
-          ? `/project/${projectId}/blob/${hash}`
-          : `/project/${projectId}/file/${id}`,
+        url: `/project/${projectId}/blob/${hash}?fallback=${id}`,
         extension: name.slice(name.lastIndexOf('.') + 1),
         extension: name.slice(name.lastIndexOf('.') + 1),
       }
       }
     }
     }

+ 1 - 7
services/web/frontend/js/features/file-view/components/file-view-header.tsx

@@ -12,7 +12,6 @@ import { LinkedFileIcon } from './file-view-icons'
 import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
 import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
 import FileViewRefreshButton from './file-view-refresh-button'
 import FileViewRefreshButton from './file-view-refresh-button'
 import FileViewRefreshError from './file-view-refresh-error'
 import FileViewRefreshError from './file-view-refresh-error'
-import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 import MaterialIcon from '@/shared/components/material-icon'
 import MaterialIcon from '@/shared/components/material-icon'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
 import OLButton from '@/features/ui/components/ol/ol-button'
 import OLButton from '@/features/ui/components/ol/ol-button'
@@ -53,7 +52,6 @@ type FileViewHeaderProps = {
 export default function FileViewHeader({ file }: FileViewHeaderProps) {
 export default function FileViewHeader({ file }: FileViewHeaderProps) {
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
   const { fileTreeReadOnly } = useFileTreeData()
   const { fileTreeReadOnly } = useFileTreeData()
-  const { fileTreeFromHistory } = useSnapshotContext()
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 
   const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
   const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
@@ -86,11 +84,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
         <OLButton
         <OLButton
           variant="secondary"
           variant="secondary"
           download={file.name}
           download={file.name}
-          href={
-            fileTreeFromHistory
-              ? `/project/${projectId}/blob/${file.hash}`
-              : `/project/${projectId}/file/${file.id}`
-          }
+          href={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
         >
         >
           <BootstrapVersionSwitcher
           <BootstrapVersionSwitcher
             bs3={<Icon type="download" fw />}
             bs3={<Icon type="download" fw />}

+ 1 - 7
services/web/frontend/js/features/file-view/components/file-view-image.tsx

@@ -1,5 +1,4 @@
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useProjectContext } from '../../../shared/context/project-context'
-import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
 
 
 export default function FileViewImage({
 export default function FileViewImage({
@@ -12,15 +11,10 @@ export default function FileViewImage({
   onError: () => void
   onError: () => void
 }) {
 }) {
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
-  const { fileTreeFromHistory } = useSnapshotContext()
 
 
   return (
   return (
     <img
     <img
-      src={
-        fileTreeFromHistory
-          ? `/project/${projectId}/blob/${file.hash}`
-          : `/project/${projectId}/file/${file.id}`
-      }
+      src={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
       onLoad={onLoad}
       onLoad={onLoad}
       onError={onError}
       onError={onError}
       alt={file.name}
       alt={file.name}

+ 14 - 24
services/web/frontend/js/features/file-view/components/file-view-text.tsx

@@ -3,7 +3,6 @@ import { useProjectContext } from '../../../shared/context/project-context'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import useAbortController from '../../../shared/hooks/use-abort-controller'
 import useAbortController from '../../../shared/hooks/use-abort-controller'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
 import { BinaryFile } from '@/features/file-view/types/binary-file'
-import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 
 const MAX_FILE_SIZE = 2 * 1024 * 1024
 const MAX_FILE_SIZE = 2 * 1024 * 1024
 
 
@@ -17,7 +16,6 @@ export default function FileViewText({
   onError: () => void
   onError: () => void
 }) {
 }) {
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
-  const { fileTreeFromHistory } = useSnapshotContext()
 
 
   const [textPreview, setTextPreview] = useState('')
   const [textPreview, setTextPreview] = useState('')
   const [shouldShowDots, setShouldShowDots] = useState(false)
   const [shouldShowDots, setShouldShowDots] = useState(false)
@@ -30,9 +28,7 @@ export default function FileViewText({
     if (inFlight) {
     if (inFlight) {
       return
       return
     }
     }
-    let path = fileTreeFromHistory
-      ? `/project/${projectId}/blob/${file.hash}`
-      : `/project/${projectId}/file/${file.id}`
+    const path = `/project/${projectId}/blob/${file.hash}?fallback=${file.id}`
     const fetchContentLengthTimeout = setTimeout(
     const fetchContentLengthTimeout = setTimeout(
       () => fetchContentLengthController.abort(),
       () => fetchContentLengthController.abort(),
       10000
       10000
@@ -45,32 +41,27 @@ export default function FileViewText({
       })
       })
       .then(fileSize => {
       .then(fileSize => {
         let truncated = false
         let truncated = false
-        let maxSize = null
+        const headers = new Headers()
         if (fileSize && Number(fileSize) > MAX_FILE_SIZE) {
         if (fileSize && Number(fileSize) > MAX_FILE_SIZE) {
           truncated = true
           truncated = true
-          maxSize = MAX_FILE_SIZE
-        }
-
-        if (maxSize != null) {
-          path += `?range=0-${maxSize}`
+          headers.set('Range', `bytes=0-${MAX_FILE_SIZE}`)
         }
         }
         fetchDataTimeout = window.setTimeout(
         fetchDataTimeout = window.setTimeout(
           () => fetchDataController.abort(),
           () => fetchDataController.abort(),
           60000
           60000
         )
         )
-        return fetch(path, { signal: fetchDataController.signal }).then(
-          response => {
-            return response.text().then(text => {
-              if (truncated) {
-                text = text.replace(/\n.*$/, '')
-              }
+        const signal = fetchDataController.signal
+        return fetch(path, { signal, headers }).then(response => {
+          return response.text().then(text => {
+            if (truncated) {
+              text = text.replace(/\n.*$/, '')
+            }
 
 
-              setTextPreview(text)
-              onLoad()
-              setShouldShowDots(truncated)
-            })
-          }
-        )
+            setTextPreview(text)
+            onLoad()
+            setShouldShowDots(truncated)
+          })
+        })
       })
       })
       .catch(err => {
       .catch(err => {
         debugConsole.error('Error fetching file contents', err)
         debugConsole.error('Error fetching file contents', err)
@@ -82,7 +73,6 @@ export default function FileViewText({
         clearTimeout(fetchDataTimeout)
         clearTimeout(fetchDataTimeout)
       })
       })
   }, [
   }, [
-    fileTreeFromHistory,
     projectId,
     projectId,
     file.id,
     file.id,
     file.hash,
     file.hash,

+ 18 - 23
services/web/frontend/stories/file-view/file-view.stories.jsx

@@ -21,7 +21,7 @@ extra parameters or packages included.
 
 
 const setupFetchMock = fetchMock => {
 const setupFetchMock = fetchMock => {
   return fetchMock
   return fetchMock
-    .head('express:/project/:project_id/file/:file_id', {
+    .head('express:/project/:project_id/blob/:hash', {
       status: 201,
       status: 201,
       headers: { 'Content-Length': 10000 },
       headers: { 'Content-Length': 10000 },
     })
     })
@@ -41,10 +41,9 @@ const fileData = {
 
 
 export const FileFromUrl = args => {
 export const FileFromUrl = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.latex }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.latex,
+    })
   )
   )
 
 
   return <FileView {...args} />
   return <FileView {...args} />
@@ -61,10 +60,9 @@ FileFromUrl.args = {
 
 
 export const FileFromProjectWithLinkableProjectId = args => {
 export const FileFromProjectWithLinkableProjectId = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.latex }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.latex,
+    })
   )
   )
 
 
   return <FileView {...args} />
   return <FileView {...args} />
@@ -82,10 +80,9 @@ FileFromProjectWithLinkableProjectId.args = {
 
 
 export const FileFromProjectWithoutLinkableProjectId = args => {
 export const FileFromProjectWithoutLinkableProjectId = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.latex }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.latex,
+    })
   )
   )
 
 
   return <FileView {...args} />
   return <FileView {...args} />
@@ -103,10 +100,9 @@ FileFromProjectWithoutLinkableProjectId.args = {
 
 
 export const FileFromProjectOutputWithLinkableProject = args => {
 export const FileFromProjectOutputWithLinkableProject = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.latex }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.latex,
+    })
   )
   )
 
 
   return <FileView {...args} />
   return <FileView {...args} />
@@ -124,10 +120,9 @@ FileFromProjectOutputWithLinkableProject.args = {
 
 
 export const FileFromProjectOutputWithoutLinkableProjectId = args => {
 export const FileFromProjectOutputWithoutLinkableProjectId = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.latex }
-    )
+    setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
+      body: bodies.latex,
+    })
   )
   )
 
 
   return <FileView {...args} />
   return <FileView {...args} />
@@ -165,7 +160,7 @@ ImageFile.args = {
 export const TextFile = args => {
 export const TextFile = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
     setupFetchMock(fetchMock).get(
     setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
+      'express:/project/:project_id/blob/:hash',
       { body: bodies.text },
       { body: bodies.text },
       { overwriteRoutes: true }
       { overwriteRoutes: true }
     )
     )
@@ -187,7 +182,7 @@ TextFile.args = {
 export const UploadedFile = args => {
 export const UploadedFile = args => {
   useFetchMock(fetchMock =>
   useFetchMock(fetchMock =>
     setupFetchMock(fetchMock).head(
     setupFetchMock(fetchMock).head(
-      'express:/project/:project_id/file/:file_id',
+      'express:/project/:project_id/blob/:hash',
       { status: 500 },
       { status: 500 },
       { overwriteRoutes: true }
       { overwriteRoutes: true }
     )
     )

+ 2 - 15
services/web/test/frontend/features/file-tree/util/path.test.ts

@@ -151,23 +151,10 @@ describe('Path utils', function () {
       const preview = previewByPath(
       const preview = previewByPath(
         rootFolder,
         rootFolder,
         'test-project-id',
         'test-project-id',
-        'test-folder/example.png',
-        false
+        'test-folder/example.png'
       )
       )
       expect(preview).to.deep.equal({
       expect(preview).to.deep.equal({
-        url: '/project/test-project-id/file/test-file-in-folder',
-        extension: 'png',
-      })
-    })
-    it('returns handles history file-tree', function () {
-      const preview = previewByPath(
-        rootFolder,
-        'test-project-id',
-        'test-folder/example.png',
-        true
-      )
-      expect(preview).to.deep.equal({
-        url: '/project/test-project-id/blob/42',
+        url: '/project/test-project-id/blob/42?fallback=test-file-in-folder',
         extension: 'png',
         extension: 'png',
       })
       })
     })
     })

+ 2 - 2
services/web/test/frontend/features/file-view/components/file-view-text.test.jsx

@@ -23,12 +23,12 @@ describe('<FileViewText/>', function () {
   })
   })
 
 
   it('renders a text view', async function () {
   it('renders a text view', async function () {
-    fetchMock.head('express:/project/:project_id/file/:file_id', {
+    fetchMock.head('express:/project/:project_id/blob/:hash', {
       status: 201,
       status: 201,
       headers: { 'Content-Length': 10000 },
       headers: { 'Content-Length': 10000 },
     })
     })
     fetchMock.get(
     fetchMock.get(
-      'express:/project/:project_id/file/:file_id',
+      'express:/project/:project_id/blob/:hash',
       'Text file content'
       'Text file content'
     )
     )
 
 

+ 2 - 2
services/web/test/frontend/features/file-view/components/file-view.test.jsx

@@ -36,12 +36,12 @@ describe('<FileView/>', function () {
 
 
   describe('for a text file', function () {
   describe('for a text file', function () {
     it('shows a loading indicator while the file is loading', async function () {
     it('shows a loading indicator while the file is loading', async function () {
-      fetchMock.head('express:/project/:project_id/file/:file_id', {
+      fetchMock.head('express:/project/:project_id/blob/:hash', {
         status: 201,
         status: 201,
         headers: { 'Content-Length': 10000 },
         headers: { 'Content-Length': 10000 },
       })
       })
       fetchMock.get(
       fetchMock.get(
-        'express:/project/:project_id/file/:file_id',
+        'express:/project/:project_id/blob/:hash',
         'Text file content'
         'Text file content'
       )
       )