mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
[web] prepare filestore migration for Server Pro/CE (#27230)
* [web] prepare filestore migration for Server Pro/CE * [history-v1] remove unused USER_FILES_BUCKET_NAME env var from script * [server-ce] tests: write default docker-compose.override.yml on startup * [server-ce] tests: extend access logging of host-admin for response * [server-ce] tests: test text and binary file upload * [server-ce] tests: add tests for filestore migration * [web] simplify feature gate for filestore/project-history-blobs logic Co-authored-by: Brian Gough <brian.gough@overleaf.com> * [server-ce] test: fix flaky test helper --------- Co-authored-by: Brian Gough <brian.gough@overleaf.com> GitOrigin-RevId: f89bdab2749e2b7a49d609e2eac6bf621c727966
This commit is contained in:
@@ -21,9 +21,11 @@ test-e2e-native:
|
||||
|
||||
test-e2e:
|
||||
docker compose build host-admin
|
||||
docker compose up -d host-admin
|
||||
docker compose up --no-log-prefix --exit-code-from=e2e e2e
|
||||
|
||||
test-e2e-open:
|
||||
docker compose up -d host-admin
|
||||
docker compose up --no-log-prefix --exit-code-from=e2e-open e2e-open
|
||||
|
||||
clean:
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createNewFile,
|
||||
createProject,
|
||||
openProjectById,
|
||||
testNewFileUpload,
|
||||
} from './helpers/project'
|
||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
||||
import { ensureUserExists, login } from './helpers/login'
|
||||
@@ -119,24 +120,7 @@ describe('editor', () => {
|
||||
cy.get('button').contains('New file').click({ force: true })
|
||||
})
|
||||
|
||||
it('can upload file', () => {
|
||||
const name = `${uuid()}.txt`
|
||||
const content = `Test File Content ${name}`
|
||||
cy.get('button').contains('Upload').click({ force: true })
|
||||
cy.get('input[type=file]')
|
||||
.first()
|
||||
.selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from(content),
|
||||
fileName: name,
|
||||
lastModified: Date.now(),
|
||||
},
|
||||
{ force: true }
|
||||
)
|
||||
// force: The file-tree pane is too narrow to display the full name.
|
||||
cy.findByTestId('file-tree').findByText(name).click({ force: true })
|
||||
cy.findByText(content)
|
||||
})
|
||||
testNewFileUpload()
|
||||
|
||||
it('should not display import from URL', () => {
|
||||
cy.findByText('From external URL').should('not.exist')
|
||||
|
||||
104
server-ce/test/filestore-migration.spec.ts
Normal file
104
server-ce/test/filestore-migration.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ensureUserExists, login } from './helpers/login'
|
||||
import {
|
||||
createProject,
|
||||
openProjectById,
|
||||
prepareFileUploadTest,
|
||||
} from './helpers/project'
|
||||
import { isExcludedBySharding, startWith } from './helpers/config'
|
||||
import { prepareWaitForNextCompileSlot } from './helpers/compile'
|
||||
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { purgeFilestoreData, runScript } from './helpers/hostAdminClient'
|
||||
|
||||
describe('filestore migration', function () {
|
||||
if (isExcludedBySharding('CE_CUSTOM_3')) return
|
||||
startWith({ withDataDir: true, resetData: true, vars: {} })
|
||||
ensureUserExists({ email: 'user@example.com' })
|
||||
|
||||
let projectName: string
|
||||
let projectId: string
|
||||
let waitForCompileRateLimitCoolOff: (fn: () => void) => void
|
||||
const previousBinaryFiles: (() => void)[] = []
|
||||
beforeWithReRunOnTestRetry(function () {
|
||||
projectName = `project-${uuid()}`
|
||||
login('user@example.com')
|
||||
createProject(projectName, { type: 'Example project' }).then(
|
||||
id => (projectId = id)
|
||||
)
|
||||
let queueReset
|
||||
;({ waitForCompileRateLimitCoolOff, queueReset } =
|
||||
prepareWaitForNextCompileSlot())
|
||||
queueReset()
|
||||
previousBinaryFiles.push(prepareFileUploadTest(true))
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
login('user@example.com')
|
||||
waitForCompileRateLimitCoolOff(() => {
|
||||
openProjectById(projectId)
|
||||
})
|
||||
})
|
||||
|
||||
function checkFilesAreAccessible() {
|
||||
it('can upload new binary file and read previous uploads', function () {
|
||||
previousBinaryFiles.push(prepareFileUploadTest(true))
|
||||
for (const check of previousBinaryFiles) {
|
||||
check()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders frog jpg', () => {
|
||||
cy.findByTestId('file-tree').findByText('frog.jpg').click()
|
||||
cy.get('[alt="frog.jpg"]')
|
||||
.should('be.visible')
|
||||
.and('have.prop', 'naturalWidth')
|
||||
.should('be.greaterThan', 0)
|
||||
})
|
||||
}
|
||||
|
||||
describe('OVERLEAF_FILESTORE_MIGRATION_LEVEL not set', function () {
|
||||
startWith({ withDataDir: true, vars: {} })
|
||||
checkFilesAreAccessible()
|
||||
})
|
||||
|
||||
describe('OVERLEAF_FILESTORE_MIGRATION_LEVEL=0', function () {
|
||||
startWith({
|
||||
withDataDir: true,
|
||||
vars: { OVERLEAF_FILESTORE_MIGRATION_LEVEL: '0' },
|
||||
})
|
||||
checkFilesAreAccessible()
|
||||
|
||||
describe('OVERLEAF_FILESTORE_MIGRATION_LEVEL=1', function () {
|
||||
startWith({
|
||||
withDataDir: true,
|
||||
vars: { OVERLEAF_FILESTORE_MIGRATION_LEVEL: '1' },
|
||||
})
|
||||
checkFilesAreAccessible()
|
||||
|
||||
describe('OVERLEAF_FILESTORE_MIGRATION_LEVEL=2', function () {
|
||||
startWith({
|
||||
withDataDir: true,
|
||||
vars: { OVERLEAF_FILESTORE_MIGRATION_LEVEL: '1' },
|
||||
})
|
||||
before(async function () {
|
||||
await runScript({
|
||||
cwd: 'services/history-v1',
|
||||
script: 'storage/scripts/back_fill_file_hash.mjs',
|
||||
})
|
||||
})
|
||||
startWith({
|
||||
withDataDir: true,
|
||||
vars: { OVERLEAF_FILESTORE_MIGRATION_LEVEL: '2' },
|
||||
})
|
||||
checkFilesAreAccessible()
|
||||
|
||||
describe('purge filestore data', function () {
|
||||
before(async function () {
|
||||
await purgeFilestoreData()
|
||||
})
|
||||
checkFilesAreAccessible()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ export function isExcludedBySharding(
|
||||
| 'CE_DEFAULT'
|
||||
| 'CE_CUSTOM_1'
|
||||
| 'CE_CUSTOM_2'
|
||||
| 'CE_CUSTOM_3'
|
||||
| 'PRO_DEFAULT_1'
|
||||
| 'PRO_DEFAULT_2'
|
||||
| 'PRO_CUSTOM_1'
|
||||
|
||||
@@ -85,6 +85,12 @@ export async function getRedisKeys() {
|
||||
return stdout.split('\n')
|
||||
}
|
||||
|
||||
export async function purgeFilestoreData() {
|
||||
await fetchJSON(`${hostAdminURL}/data/user_files`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
|
||||
@@ -216,3 +216,43 @@ export function createNewFile() {
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
export function prepareFileUploadTest(binary = false) {
|
||||
const name = `${uuid()}.txt`
|
||||
const content = `Test File Content ${name}${binary ? ' \x00' : ''}`
|
||||
cy.get('button').contains('Upload').click({ force: true })
|
||||
cy.get('input[type=file]')
|
||||
.first()
|
||||
.selectFile(
|
||||
{
|
||||
contents: Cypress.Buffer.from(content),
|
||||
fileName: name,
|
||||
lastModified: Date.now(),
|
||||
},
|
||||
{ force: true }
|
||||
)
|
||||
|
||||
// wait for the upload to finish
|
||||
cy.findByRole('treeitem', { name })
|
||||
|
||||
return function check() {
|
||||
cy.findByRole('treeitem', { name }).click()
|
||||
if (binary) {
|
||||
cy.findByText(content).should('not.have.class', 'cm-line')
|
||||
} else {
|
||||
cy.findByText(content).should('have.class', 'cm-line')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function testNewFileUpload() {
|
||||
it('can upload text file', () => {
|
||||
const check = prepareFileUploadTest(false)
|
||||
check()
|
||||
})
|
||||
|
||||
it('can upload binary file', () => {
|
||||
const check = prepareFileUploadTest(true)
|
||||
check()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,17 @@ const IMAGES = {
|
||||
PRO: process.env.IMAGE_TAG_PRO.replace(/:.+/, ''),
|
||||
}
|
||||
|
||||
function defaultDockerComposeOverride() {
|
||||
return {
|
||||
services: {
|
||||
sharelatex: {
|
||||
environment: {},
|
||||
},
|
||||
'git-bridge': {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let previousConfig = ''
|
||||
|
||||
function readDockerComposeOverride() {
|
||||
@@ -38,14 +49,7 @@ function readDockerComposeOverride() {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
services: {
|
||||
sharelatex: {
|
||||
environment: {},
|
||||
},
|
||||
'git-bridge': {},
|
||||
},
|
||||
}
|
||||
return defaultDockerComposeOverride
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,12 +81,21 @@ app.use(bodyParser.json())
|
||||
app.use((req, res, next) => {
|
||||
// Basic access logs
|
||||
console.log(req.method, req.url, req.body)
|
||||
const json = res.json
|
||||
res.json = body => {
|
||||
console.log(req.method, req.url, req.body, '->', body)
|
||||
json.call(res, body)
|
||||
}
|
||||
next()
|
||||
})
|
||||
app.use((req, res, next) => {
|
||||
// Add CORS headers
|
||||
const accessControlAllowOrigin =
|
||||
process.env.ACCESS_CONTROL_ALLOW_ORIGIN || 'http://sharelatex'
|
||||
res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin)
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
||||
res.setHeader('Access-Control-Max-Age', '3600')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'DELETE, GET, HEAD, POST, PUT')
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -133,6 +146,7 @@ const allowedVars = Joi.object(
|
||||
'V1_HISTORY_URL',
|
||||
'SANDBOXED_COMPILES',
|
||||
'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES',
|
||||
'OVERLEAF_FILESTORE_MIGRATION_LEVEL',
|
||||
'OVERLEAF_TEMPLATES_USER_ID',
|
||||
'OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS',
|
||||
'OVERLEAF_ALLOW_PUBLIC_ACCESS',
|
||||
@@ -319,8 +333,19 @@ app.get('/redis/keys', (req, res) => {
|
||||
)
|
||||
})
|
||||
|
||||
app.delete('/data/user_files', (req, res) => {
|
||||
runDockerCompose(
|
||||
'exec',
|
||||
['sharelatex', 'rm', '-rf', '/var/lib/overleaf/data/user_files'],
|
||||
(error, stdout, stderr) => {
|
||||
res.json({ error, stdout, stderr })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
app.use(handleValidationErrors())
|
||||
|
||||
purgeDataDir()
|
||||
writeDockerComposeOverride(defaultDockerComposeOverride())
|
||||
|
||||
app.listen(80)
|
||||
|
||||
Reference in New Issue
Block a user