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
This commit is contained in:
Jakob Ackermann
2024-11-28 09:06:37 +00:00
committed by Copybot
parent ce0d5fd383
commit faff4f1d7e
10 changed files with 46 additions and 97 deletions
@@ -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}?fallback=${selectedEntityId}`
return `/project/${projectId}/blob/${selectedEntity.entity.hash}`
}
return `/project/${projectId}/file/${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 = {
@@ -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) => (path: string) => previewByPath(fileTreeData, projectId, path),
previewByPath(fileTreeData, projectId, path, fileTreeFromHistory), [fileTreeData, projectId]
[fileTreeData, projectId, fileTreeFromHistory]
) )
const dirnameInFileTree = useCallback( const dirnameInFileTree = useCallback(
@@ -106,8 +106,7 @@ export function findEntityByPath(
export function previewByPath( export function previewByPath(
folder: Folder, folder: Folder,
projectId: string, projectId: string,
path: string, path: string
fileTreeFromHistory: boolean
): 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 url: `/project/${projectId}/blob/${hash}?fallback=${id}`,
? `/project/${projectId}/blob/${hash}`
: `/project/${projectId}/file/${id}`,
extension: name.slice(name.lastIndexOf('.') + 1), extension: name.slice(name.lastIndexOf('.') + 1),
} }
} }
@@ -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={ href={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
fileTreeFromHistory
? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${file.id}`
}
> >
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={<Icon type="download" fw />} bs3={<Icon type="download" fw />}
@@ -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={ src={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
fileTreeFromHistory
? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${file.id}`
}
onLoad={onLoad} onLoad={onLoad}
onError={onError} onError={onError}
alt={file.name} alt={file.name}
@@ -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 const path = `/project/${projectId}/blob/${file.hash}?fallback=${file.id}`
? `/project/${projectId}/blob/${file.hash}`
: `/project/${projectId}/file/${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 headers.set('Range', `bytes=0-${MAX_FILE_SIZE}`)
}
if (maxSize != null) {
path += `?range=0-${maxSize}`
} }
fetchDataTimeout = window.setTimeout( fetchDataTimeout = window.setTimeout(
() => fetchDataController.abort(), () => fetchDataController.abort(),
60000 60000
) )
return fetch(path, { signal: fetchDataController.signal }).then( const signal = fetchDataController.signal
response => { return fetch(path, { signal, headers }).then(response => {
return response.text().then(text => { return response.text().then(text => {
if (truncated) { if (truncated) {
text = text.replace(/\n.*$/, '') text = text.replace(/\n.*$/, '')
} }
setTextPreview(text) setTextPreview(text)
onLoad() onLoad()
setShouldShowDots(truncated) 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,
@@ -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( setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
'express:/project/:project_id/file/:file_id', body: bodies.latex,
{ 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( setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
'express:/project/:project_id/file/:file_id', body: bodies.latex,
{ 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( setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
'express:/project/:project_id/file/:file_id', body: bodies.latex,
{ 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( setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
'express:/project/:project_id/file/:file_id', body: bodies.latex,
{ 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( setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
'express:/project/:project_id/file/:file_id', body: bodies.latex,
{ 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 }
) )
@@ -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', 'test-folder/example.png'
false
) )
expect(preview).to.deep.equal({ expect(preview).to.deep.equal({
url: '/project/test-project-id/file/test-file-in-folder', url: '/project/test-project-id/blob/42?fallback=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',
extension: 'png', extension: 'png',
}) })
}) })
@@ -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'
) )
@@ -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'
) )