From faa07a977e0d5e7a75b44f42df849295adc243e9 Mon Sep 17 00:00:00 2001 From: Kruemmelspalter Date: Sun, 29 Jan 2023 01:48:18 +0100 Subject: [PATCH] reorganization & polish (#67) * reorganize files * add dockerignore to frontend (just copied gitignore) * extract renderer selection logic to renderer service * set render log content type to plain text * adjust height * add debug and trace logs, add option to disable caching for rendering temporarily * reformat & remove unnecessary methods * add log level options to docker compose * increase tag length limit * fix indentation --- .../backend/FileSpiderApplication.kt | 70 +++----- .../backend/api/DocumentController.kt | 51 +++--- .../database/{dao => }/CacheRepository.kt | 2 +- .../DatabaseConfiguration.kt} | 76 ++++---- .../backend/database/{model => }/Document.kt | 2 +- .../database/{dao => }/DocumentRepository.kt | 45 +++-- .../backend/database/dao/DocumentDao.kt | 29 --- .../backend/renderer/RenderCache.kt | 20 ++- .../backend/renderer/RenderService.kt | 44 +++++ .../file_spider/backend/renderer/Renderer.kt | 169 ++++++++---------- .../{ => renderer}/RenderingException.kt | 2 +- .../backend/services/DocumentMeta.kt | 2 +- .../backend/services/DocumentService.kt | 50 +++--- .../backend/services/FileSystemService.kt | 19 -- backend/app/src/main/resources/sql/init.sql | 4 +- docker-compose.yml | 10 +- frontend/.dockerignore | 90 ++++++++++ frontend/components/DocumentDisplay.vue | 2 +- frontend/package.json | 2 +- 19 files changed, 378 insertions(+), 311 deletions(-) rename backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/{dao => }/CacheRepository.kt (95%) rename backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/{FileSpiderConfiguration.kt => database/DatabaseConfiguration.kt} (50%) rename backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/{model => }/Document.kt (82%) rename backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/{dao => }/DocumentRepository.kt (64%) delete mode 100644 backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/DocumentDao.kt create mode 100644 backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderService.kt rename backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/{ => renderer}/RenderingException.kt (52%) create mode 100644 frontend/.dockerignore diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderApplication.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderApplication.kt index ef4483e..e527c3a 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderApplication.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderApplication.kt @@ -1,65 +1,45 @@ package me.kruemmelspalter.file_spider.backend -import com.typesafe.config.ConfigFactory -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration import org.springframework.boot.builder.SpringApplicationBuilder -import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.boot.web.server.ConfigurableWebServerFactory +import org.springframework.boot.web.server.WebServerFactoryCustomizer import org.springframework.context.annotation.Bean -import org.springframework.context.event.EventListener -import org.springframework.core.io.ClassPathResource -import org.springframework.core.io.support.EncodedResource -import org.springframework.jdbc.core.JdbcTemplate -import org.springframework.jdbc.datasource.init.ScriptUtils +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories import org.springframework.web.servlet.DispatcherServlet +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.util.Properties -import javax.sql.DataSource @SpringBootApplication -class FileSpiderApplication { - private val logger = LoggerFactory.getLogger(javaClass) - private val config = ConfigFactory.load() - - @Autowired - private val dataSource: DataSource? = null - - @Autowired - private val jdbcTemplate: JdbcTemplate? = null - - @EventListener(ApplicationReadyEvent::class) - fun createTablesIfNonexistent() { - jdbcTemplate!!.queryForObject( - "select count(*) from information_schema.tables where table_name in ('Document', 'Tag', 'Cache')" - ) { rs, _ -> - if (rs.getInt(1) != 3) { - logger.warn("Not all tables 'Document', 'Tag' and 'Cache' exist; creating from init script") - try { - ScriptUtils.executeSqlScript( - dataSource!!.connection, - EncodedResource(ClassPathResource(config.getString("app.initFilePath"))), - true, - false, - "--", - ";", - "##/*", - "*/##" - ) - } catch (e: Exception) { - logger.error("could not execute init script") - e.printStackTrace() - } - } - } - } +@Suppress("SpringComponentScan") +@EnableJdbcRepositories(basePackages = ["com.springdata"]) +class FileSpiderApplication : WebMvcConfigurer, WebServerFactoryCustomizer, + AbstractJdbcConfiguration() { + // enable OPTIONS request @Bean(name = [DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME]) fun dispatcherServlet(): DispatcherServlet? { val dispatcherServlet = DispatcherServlet() dispatcherServlet.setDispatchOptionsRequest(true) return dispatcherServlet } + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/libs/katex/**").addResourceLocations("classpath:/katex/") + } + + override fun customize(factory: ConfigurableWebServerFactory) { + factory.setPort(80) + } + + // basically disable CORS + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**").allowedOrigins("*").allowedHeaders("*").allowedMethods("*") + } } fun main(args: Array) { diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/api/DocumentController.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/api/DocumentController.kt index 8271c97..ac59650 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/api/DocumentController.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/api/DocumentController.kt @@ -1,6 +1,6 @@ package me.kruemmelspalter.file_spider.backend.api -import me.kruemmelspalter.file_spider.backend.RenderingException +import me.kruemmelspalter.file_spider.backend.renderer.RenderingException import me.kruemmelspalter.file_spider.backend.services.DocumentMeta import me.kruemmelspalter.file_spider.backend.services.DocumentService import me.kruemmelspalter.file_spider.backend.services.RenderedDocument @@ -33,19 +33,16 @@ import javax.servlet.http.HttpServletRequest class DocumentController { @Autowired - val documentService: DocumentService? = null + private lateinit var documentService: DocumentService @GetMapping("/") fun searchDocuments(@RequestParam("filter") filterString: String): List { - if (!Pattern.matches( - "^!?\\p{L}+(?:,!?\\p{L}+)*\$", - filterString - ) - ) throw ResponseStatusException(HttpStatus.BAD_REQUEST) + if (!Pattern.matches("^!?\\p{L}+(?:,!?\\p{L}+)*\$", filterString)) + throw ResponseStatusException(HttpStatus.BAD_REQUEST) val filters = filterString.split(",") - return documentService!!.filterDocuments( + return documentService.filterDocuments( filters.filter { !it.startsWith("!") }, filters.filter { it.startsWith("!") }.map { it.substring(1) } ) @@ -62,39 +59,41 @@ class DocumentController { @RequestParam fileExtension: String?, ): String { if (mimeType == null && file == null) throw ResponseStatusException(HttpStatus.BAD_REQUEST) - val uuid = if (file != null) - documentService!!.createDocument(title, renderer, editor, mimeType ?: file.contentType, tags, fileExtension, file.inputStream) - else documentService!!.createDocument(title, renderer, editor, mimeType!!, tags, fileExtension, null) + val uuid = if (file != null) documentService.createDocument( + title, renderer, editor, mimeType ?: file.contentType, tags, fileExtension, file.inputStream + ) + else documentService.createDocument(title, renderer, editor, mimeType!!, tags, fileExtension, null) return uuid.toString() } @GetMapping("/{id}") fun getDocumentMeta(@PathVariable("id") documentId: UUID): DocumentMeta { - return documentService!!.getDocumentMeta(documentId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + return documentService.getDocumentMeta(documentId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } data class DocumentChange(val addTags: List?, val removeTags: List?, val title: String?) @PatchMapping("/{id}") fun changeDocumentTags(@PathVariable id: UUID, @RequestBody change: DocumentChange) { - if (change.addTags != null) documentService!!.addTags(id, change.addTags) - if (change.removeTags != null) documentService!!.removeTags(id, change.removeTags) - if (change.title != null) documentService!!.setTitle(id, change.title) + if (change.addTags != null) documentService.addTags(id, change.addTags) + if (change.removeTags != null) documentService.removeTags(id, change.removeTags) + if (change.title != null) documentService.setTitle(id, change.title) } @DeleteMapping("/{id}") fun deleteDocument(@PathVariable id: UUID) { - documentService!!.deleteDocument(id) + documentService.deleteDocument(id) } @GetMapping("/{id}/rendered") fun getRenderedDocument( @PathVariable("id") documentId: UUID, - @RequestParam("download") download: Boolean? + @RequestParam download: Boolean?, + @RequestParam cache: Boolean?, ): ResponseEntity { - val renderedDocument: RenderedDocument = - documentService!!.renderDocument(documentId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + val renderedDocument: RenderedDocument = documentService.renderDocument(documentId, cache ?: true) + ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) val headers = HttpHeaders() headers.contentType = MediaType.parseMediaType(renderedDocument.contentType) @@ -102,10 +101,7 @@ class DocumentController { if (download == true) headers.contentDisposition = ContentDisposition.parse("attachment; filename=" + renderedDocument.fileName) - return ResponseEntity - .status(HttpStatus.OK) - .headers(headers) - .body(InputStreamResource(renderedDocument.stream)) + return ResponseEntity.status(HttpStatus.OK).headers(headers).body(InputStreamResource(renderedDocument.stream)) } @GetMapping("/{id}/rendered/{file}") @@ -114,18 +110,21 @@ class DocumentController { @PathVariable("file") fileName: String, request: HttpServletRequest ): Resource? { - return documentService!!.getDocumentResource(documentId, fileName, request.servletContext) + return documentService.getDocumentResource(documentId, fileName, request.servletContext) } @GetMapping("/{id}/renderlog") fun getRenderLog(@PathVariable id: UUID): InputStreamResource { return InputStreamResource( - documentService!!.readDocumentLog(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + documentService.readDocumentLog(id) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) ) } @ExceptionHandler(value = [RenderingException::class]) fun renderingException(exception: RenderingException?): ResponseEntity { - return ResponseEntity(exception!!.message, HttpStatus.INTERNAL_SERVER_ERROR) + return ResponseEntity + .internalServerError() + .contentType(MediaType.TEXT_PLAIN) + .body(exception?.message) } } diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/CacheRepository.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/CacheRepository.kt similarity index 95% rename from backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/CacheRepository.kt rename to backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/CacheRepository.kt index b7c6a50..a99e56b 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/CacheRepository.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/CacheRepository.kt @@ -1,4 +1,4 @@ -package me.kruemmelspalter.file_spider.backend.database.dao +package me.kruemmelspalter.file_spider.backend.database import org.springframework.beans.factory.annotation.Autowired import org.springframework.dao.support.DataAccessUtils diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderConfiguration.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/DatabaseConfiguration.kt similarity index 50% rename from backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderConfiguration.kt rename to backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/DatabaseConfiguration.kt index dc7a29c..4694a89 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/FileSpiderConfiguration.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/DatabaseConfiguration.kt @@ -1,30 +1,57 @@ -package me.kruemmelspalter.file_spider.backend +package me.kruemmelspalter.file_spider.backend.database -import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import org.springframework.boot.web.server.ConfigurableWebServerFactory -import org.springframework.boot.web.server.WebServerFactoryCustomizer +import org.slf4j.LoggerFactory +import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration -import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories +import org.springframework.context.event.EventListener +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.support.EncodedResource import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import org.springframework.jdbc.datasource.DataSourceTransactionManager import org.springframework.jdbc.datasource.DriverManagerDataSource +import org.springframework.jdbc.datasource.init.ScriptUtils import org.springframework.transaction.PlatformTransactionManager -import org.springframework.web.servlet.config.annotation.CorsRegistry -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import javax.sql.DataSource -@Suppress("SpringComponentScan") @Configuration -@EnableJdbcRepositories(basePackages = ["com.springdata"]) -class FileSpiderConfiguration : WebMvcConfigurer, WebServerFactoryCustomizer, - AbstractJdbcConfiguration() { - val config: Config = ConfigFactory.load() +class DatabaseConfiguration { + private val logger = LoggerFactory.getLogger(javaClass) + private val config = ConfigFactory.load() + + @EventListener(ApplicationReadyEvent::class) + fun createTablesIfNonexistent() { + jdbcTemplate().queryForObject( + "select count(*) from information_schema.tables where table_name in ('Document', 'Tag', 'Cache')" + ) { rs, _ -> + if (rs.getInt(1) != 3) { + logger.warn("Not all tables 'Document', 'Tag' and 'Cache' exist; creating from init script") + try { + ScriptUtils.executeSqlScript( + dataSource().connection, + EncodedResource(ClassPathResource(config.getString("app.initFilePath"))), + true, + false, + "--", + ";", + "##/*", + "*/##" + ) + } catch (e: Exception) { + logger.error("could not execute init script") + e.printStackTrace() + } + } + } + } + + @Bean + fun jdbcTemplate(): JdbcTemplate { + return JdbcTemplate(dataSource()) + } @Bean fun dataSource(): DataSource { @@ -38,30 +65,13 @@ class FileSpiderConfiguration : WebMvcConfigurer, WebServerFactoryCustomizer documentFromResultSet(rs) }, id.toString() ) if (documents.size != 1) return null return documents[0] } - override fun getTags(id: UUID): List { - return jdbcTemplate!!.query( + fun getTags(id: UUID): List { + return jdbcTemplate.query( "select tag from Tag where document = ?", { rs, _ -> rs.getString(1) }, id.toString() ) } - override fun deleteDocument(id: UUID) { - jdbcTemplate!!.update("delete from Document where id=?", id.toString()) - jdbcTemplate!!.update("delete from Tag where document=?", id.toString()) + fun deleteDocument(id: UUID) { + jdbcTemplate.update("delete from Document where id=?", id.toString()) + jdbcTemplate.update("delete from Tag where document=?", id.toString()) } - override fun filterDocuments(posFilter: List, negFilter: List): List { + fun filterDocuments(posFilter: List, negFilter: List): List { if (posFilter.isEmpty()) return ArrayList() - return namedParameterJdbcOperations!!.query( + return namedParameterJdbcOperations.query( "select Document.* from Document left join (select document, count(tag) as tagCount from Tag where " + "tag in (:posTags) group by document) as postags on postags.document = Document.id " + ( @@ -53,20 +52,20 @@ class DocumentRepository : DocumentDao { ) { rs, _ -> documentFromResultSet(rs) } } - override fun removeTags(id: UUID, removeTags: List) { - jdbcTemplate!!.batchUpdate("delete from Tag where document=? and tag=?", removeTags, 100) { ps, s -> + fun removeTags(id: UUID, removeTags: List) { + jdbcTemplate.batchUpdate("delete from Tag where document=? and tag=?", removeTags, 100) { ps, s -> ps.setString(1, id.toString()); ps.setString(2, s) } } - override fun addTags(id: UUID, addTags: List) { - jdbcTemplate!!.batchUpdate("insert into Tag(document, tag) values (?,?)", addTags, 100) { ps, s -> + fun addTags(id: UUID, addTags: List) { + jdbcTemplate.batchUpdate("insert into Tag(document, tag) values (?,?)", addTags, 100) { ps, s -> ps.setString(1, id.toString()); ps.setString(2, s) } } - override fun setTitle(id: UUID, title: String) { - jdbcTemplate!!.update("update Document set title=? where id=?", title, id.toString()) + fun setTitle(id: UUID, title: String) { + jdbcTemplate.update("update Document set title=? where id=?", title, id.toString()) } private fun documentFromResultSet(rs: ResultSet) = Document( @@ -79,7 +78,7 @@ class DocumentRepository : DocumentDao { rs.getString(7) ) - override fun createDocument( + fun createDocument( uuid: UUID, title: String, renderer: String, @@ -89,7 +88,7 @@ class DocumentRepository : DocumentDao { fileExtension: String?, ) { - if (jdbcTemplate!!.update( + if (jdbcTemplate.update( "insert into Document (id, title, renderer, editor, mimeType, fileExtension) values (?,?,?,?,?,?)", uuid.toString(), title, @@ -100,7 +99,7 @@ class DocumentRepository : DocumentDao { ) != 1 ) throw RuntimeException("document creation did not work / didn't affect one row") - jdbcTemplate!!.batchUpdate("insert into Tag (tag, document) values (?, ?)", tags, 100) { ps, s -> + jdbcTemplate.batchUpdate("insert into Tag (tag, document) values (?, ?)", tags, 100) { ps, s -> ps.setString(1, s); ps.setString(2, uuid.toString()) } } diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/DocumentDao.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/DocumentDao.kt deleted file mode 100644 index 7b38b5e..0000000 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/database/dao/DocumentDao.kt +++ /dev/null @@ -1,29 +0,0 @@ -package me.kruemmelspalter.file_spider.backend.database.dao - -import me.kruemmelspalter.file_spider.backend.database.model.Document -import java.util.UUID - -interface DocumentDao { - - fun getDocument(id: UUID): Document? - - fun getTags(id: UUID): List - - fun createDocument( - uuid: UUID, - title: String, - renderer: String, - editor: String, - mimeType: String, - tags: List, - fileExtension: String?, - ) - - fun deleteDocument(id: UUID) - - fun filterDocuments(posFilter: List, negFilter: List): List - fun removeTags(id: UUID, removeTags: List) - fun addTags(id: UUID, addTags: List) - - fun setTitle(id: UUID, title: String) -} diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderCache.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderCache.kt index 09cfbdb..981ef50 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderCache.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderCache.kt @@ -1,8 +1,9 @@ package me.kruemmelspalter.file_spider.backend.renderer -import me.kruemmelspalter.file_spider.backend.database.dao.CacheRepository +import me.kruemmelspalter.file_spider.backend.database.CacheRepository import me.kruemmelspalter.file_spider.backend.services.FileSystemService import me.kruemmelspalter.file_spider.backend.services.RenderedDocument +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import java.io.FileInputStream @@ -17,6 +18,8 @@ import java.util.UUID @Service class RenderCache { + private val logger = LoggerFactory.getLogger(javaClass) + @Autowired private lateinit var fsService: FileSystemService @@ -24,11 +27,17 @@ class RenderCache { private lateinit var cacheRepository: CacheRepository fun isCacheValid(id: UUID): Boolean { - return computeHash(id).contentEquals(cacheRepository.getCacheEntry(id)?.hash) + logger.debug("validating cache for id $id") + val computedHash = computeHash(id) + val cachedHash = cacheRepository.getCacheEntry(id)?.hash + val isValid = computedHash.contentEquals(cachedHash) + logger.debug("computed: ${computedHash.toHex()} cached: ${cachedHash?.toHex()} valid: $isValid") + return isValid } fun getCachedRender(id: UUID): RenderedDocument? { val cacheEntry = cacheRepository.getCacheEntry(id) ?: return null + logger.trace("serving cached file ${Paths.get(fsService.getCacheDirectory().toString(), id.toString())}") return RenderedDocument( FileInputStream(Paths.get(fsService.getCacheDirectory().toString(), id.toString()).toFile()), cacheEntry.mimeType, @@ -41,6 +50,7 @@ class RenderCache { } fun cacheRender(id: UUID, renderedDocument: RenderedDocument?): RenderedDocument? { + logger.debug("setting cache entry for id $id") if (renderedDocument == null) return null val cacheFile = Paths.get(fsService.getCacheDirectory().toString(), id.toString()).toFile() val outStream = FileOutputStream(cacheFile) @@ -50,16 +60,18 @@ class RenderCache { return RenderedDocument( FileInputStream(cacheFile), renderedDocument.contentType, - renderedDocument.contentLength, + Files.readAttributes(cacheFile.toPath(), BasicFileAttributes::class.java).size(), renderedDocument.fileName ) } private fun computeHash(id: UUID): ByteArray { + logger.debug("computing hash for id $id") val md = MessageDigest.getInstance("MD5") val directory = fsService.getDirectoryPathFromID(id) val files = Files.find(directory, 5, { _, _ -> true }) files.forEach { path -> + logger.trace("adding file $path") md.update(path.toString().toByteArray()) val attributes = Files.readAttributes(path, BasicFileAttributes::class.java) md.update(longToByteArray(attributes.size())) @@ -73,4 +85,6 @@ class RenderCache { ByteBuffer.wrap(bytes).putLong(value) return bytes.copyOfRange(4, 8) } + + private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } } diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderService.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderService.kt new file mode 100644 index 0000000..99ea551 --- /dev/null +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderService.kt @@ -0,0 +1,44 @@ +package me.kruemmelspalter.file_spider.backend.renderer + +import me.kruemmelspalter.file_spider.backend.database.Document +import me.kruemmelspalter.file_spider.backend.services.FileSystemService +import me.kruemmelspalter.file_spider.backend.services.RenderedDocument +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service + +@Service +class RenderService { + + private val logger = LoggerFactory.getLogger(javaClass) + + @Autowired + private lateinit var fsService: FileSystemService + + @Autowired + private lateinit var renderCache: RenderCache + + fun renderDocument(document: Document, useCache: Boolean = true): RenderedDocument? { + return getRenderer(document).render(document, fsService, renderCache, useCache) + } + + fun getRenderer(document: Document): Renderer { + logger.debug("Getting renderer from renderer ${document.renderer} and mime type ${document.mimeType}") + return when (document.renderer) { + "plain" -> Renderer.plainRenderer + "markdown", "md" -> Renderer.markdownRenderer + "tex", "latex" -> Renderer.latexRenderer + "xournal", "xournalpp" -> Renderer.xournalppRenderer + "html" -> Renderer.htmlRenderer + "renderer" -> Renderer.ebookRenderer + else -> when (document.mimeType) { + "application/x-tex", "application/x-latex" -> Renderer.latexRenderer + "text/markdown" -> Renderer.markdownRenderer + "application/x-xopp" -> Renderer.xournalppRenderer + "text/html" -> Renderer.htmlRenderer + "application/zip+epub" -> Renderer.ebookRenderer + else -> Renderer.plainRenderer + } + } + } +} diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/Renderer.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/Renderer.kt index c27c48e..0cac9be 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/Renderer.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/Renderer.kt @@ -1,12 +1,11 @@ package me.kruemmelspalter.file_spider.backend.renderer -import me.kruemmelspalter.file_spider.backend.RenderingException -import me.kruemmelspalter.file_spider.backend.database.model.Document +import me.kruemmelspalter.file_spider.backend.database.Document import me.kruemmelspalter.file_spider.backend.services.FileSystemService import me.kruemmelspalter.file_spider.backend.services.RenderedDocument +import org.slf4j.LoggerFactory import org.springframework.util.FileSystemUtils import java.io.FileInputStream -import java.io.InputStream import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -14,11 +13,13 @@ import java.nio.file.attribute.BasicFileAttributes import java.util.Date import java.util.concurrent.TimeUnit -class Renderer { +class Renderer(private val name: String) { private val renderSteps: MutableList<(RenderMeta) -> Unit> = mutableListOf() + private val logger = LoggerFactory.getLogger(javaClass) + companion object { - private val plainRenderer = Renderer().output { + internal val plainRenderer = Renderer("plain").output { RenderedDocument( FileInputStream(it.fsService.getFileFromID(it.document.id, it.document.fileExtension)), it.document.mimeType, @@ -27,38 +28,37 @@ class Renderer { ) } - private val htmlRenderer = - Renderer().tempDir().resolveLinks().outputFile("text/html", "html") { it.fileName } - - private val markdownRenderer = - Renderer().tempDir() - .copy({ it.fileName }, { "tmp0.md" }) - .command(2) { - listOf( - "mmdc", - "-p", "/opt/filespider/mmdc-puppeteer-config.json", - "-i", "tmp0.md", - "-o", "tmp1.md", - "-e", "png", - "-w", "19200px", - ) - } - .replace(mapOf("!\\[diagram\\]\\((\\.\\/tmp1-[0-9]+\\.png)\\)" to "")) { "tmp1.md" } - .command(10) { - listOf( - "pandoc", - "-f", "markdown", - "-o", "out.html", - "-s", - "--katex=http://localhost:80/libs/katex/", - "--metadata", "title=${it.document.title}", - "--self-contained", - "tmp1.md" - ) - } - .resolveLinks { "out.html" }.outputFile("text/html", "html") { "out.html" } - - private val latexRenderer = Renderer().tempDir().command(10) { + internal val htmlRenderer = + Renderer("html").tempDir().resolveLinks().outputFile("text/html", "html") { it.fileName } + + internal val markdownRenderer = Renderer("markdown").tempDir().copy({ it.fileName }, { "tmp0.md" }).command(2) { + listOf( + "mmdc", + "-p", "/opt/filespider/mmdc-puppeteer-config.json", + "-i", "tmp0.md", + "-o", "tmp1.md", + "-e", "png", + "-w", "19200px", + ) + } + .replace(mapOf("!\\[diagram\\]\\((\\.\\/tmp1-[0-9]+\\.png)\\)" to "")) { "tmp1.md" } + .command(10) { + listOf( + "pandoc", + "-f", + "markdown", + "-o", + "out.html", + "-s", + "--katex=http://localhost:80/libs/katex/", + "--metadata", + "title=${it.document.title}", + "--self-contained", + "tmp1.md" + ) + }.resolveLinks { "out.html" }.outputFile("text/html", "html") { "out.html" } + + internal val latexRenderer = Renderer("latex").tempDir().command(10) { listOf( "bash", "-c", @@ -66,45 +66,19 @@ class Renderer { ) }.outputFile("application/pdf", "pdf") { "out.pdf" } - private val xournalppRenderer = - Renderer().tempDir().command(2) { - listOf( - "sh", - "-c", - "gunzip -c -S .${it.document.fileExtension} ${it.fileName} |sed -r -e 's/filename=\".*\\/${it.document.id}\\/(.*)\" /filename=\"\\1\" /g'|gzip>tmp.xopp" - ) - } - .command(10) { listOf("xournalpp", "-p", "out.pdf", "tmp.xopp") } - .outputFile("application/pdf", "pdf") { "out.pdf" } - - private val ebookRenderer = Renderer().tempDir() - .command(15, mapOf("QTWEBENGINE_CHROMIUM_FLAGS" to "--no-sandbox")) { - listOf("ebook-convert", it.fileName, "out.pdf") - } + internal val xournalppRenderer = Renderer("xournalpp").tempDir().command(2) { + listOf( + "sh", + "-c", + "gunzip -c -S .${it.document.fileExtension} ${it.fileName} |sed -r -e 's/filename=\".*\\/${it.document.id}\\/(.*)\" /filename=\"\\1\" /g'|gzip>tmp.xopp" + ) + }.command(10) { listOf("xournalpp", "-p", "out.pdf", "tmp.xopp") } .outputFile("application/pdf", "pdf") { "out.pdf" } - private val mimeSpecificRenderer = Renderer().useRenderer { - when (it.document.mimeType) { - "application/x-tex", "application/x-latex" -> latexRenderer - "text/markdown" -> markdownRenderer - "application/x-xopp" -> xournalppRenderer - "text/html" -> htmlRenderer - "application/zip+epub" -> ebookRenderer - else -> plainRenderer - } - } - - fun getRenderer(rendererName: String): Renderer { - return when (rendererName) { - "plain" -> plainRenderer - "markdown", "md" -> markdownRenderer - "tex", "latex" -> latexRenderer - "xournal", "xournalpp" -> xournalppRenderer - "html" -> htmlRenderer - "renderer" -> ebookRenderer - else -> mimeSpecificRenderer - } - } + internal val ebookRenderer = + Renderer("ebook").tempDir().command(15, mapOf("QTWEBENGINE_CHROMIUM_FLAGS" to "--no-sandbox")) { + listOf("ebook-convert", it.fileName, "out.pdf") + }.outputFile("application/pdf", "pdf") { "out.pdf" } } fun addStep(step: (RenderMeta) -> Unit): Renderer { @@ -112,14 +86,6 @@ class Renderer { return this } - fun output(stream: InputStream, mimeType: String, contentLength: Long, fileName: String): Renderer { - return output { RenderedDocument(stream, mimeType, contentLength, fileName) } - } - - fun output(document: RenderedDocument): Renderer { - return output { document } - } - fun output(documentProvider: (RenderMeta) -> RenderedDocument): Renderer { return addStep { it.output = documentProvider(it) } } @@ -127,6 +93,7 @@ class Renderer { fun outputFile(mimeType: String, fileExtension: String, filenameProvider: (RenderMeta) -> String): Renderer { return output { val filename = filenameProvider(it) + logger.trace("serving file ${Paths.get(it.workingDirectory.toString(), filename)}") RenderedDocument( FileInputStream(Paths.get(it.workingDirectory.toString(), filename).toString()), mimeType, @@ -150,8 +117,11 @@ class Renderer { .directory(it.workingDirectory.toFile()) environment.toMap(pb.environment()) val process = pb.start() - if (!process.waitFor(timeout, timeUnit) || process.exitValue() != 0) - throw RenderingException(String(it.fsService.readLog(it.document.id)?.readAllBytes() ?: byteArrayOf())) + if (!process.waitFor( + timeout, + timeUnit + ) || process.exitValue() != 0 + ) throw RenderingException(String(it.fsService.readLog(it.document.id)?.readAllBytes() ?: byteArrayOf())) } } @@ -160,6 +130,7 @@ class Renderer { val tmpPath = Paths.get( it.fsService.getTemporaryDirectory().toString(), "filespider-${it.document.id}-${Date().time}" ) + logger.trace("establishing temp dir at $tmpPath") Files.createDirectories(tmpPath) FileSystemUtils.copyRecursively(it.fsService.getDirectoryPathFromID(it.document.id), tmpPath) it.workingDirectory = tmpPath @@ -172,14 +143,8 @@ class Renderer { filenameProvider: (RenderMeta) -> String = { it.fileName } ): Renderer { return command(1) { meta -> - listOf("sed", "-i", "-E") + replacements.map { listOf("-e", "s/${it.key}/${it.value}/g") }.flatten() + - listOf(Paths.get(meta.workingDirectory.toString(), filenameProvider(meta)).toString()) - } - } - - fun useRenderer(rendererProvider: (RenderMeta) -> Renderer): Renderer { - return addStep { - it.output = rendererProvider(it).render(it.document, it.fsService, it.cache) + listOf("sed", "-i", "-E") + replacements.map { listOf("-e", "s/${it.key}/${it.value}/g") } + .flatten() + listOf(Paths.get(meta.workingDirectory.toString(), filenameProvider(meta)).toString()) } } @@ -187,8 +152,7 @@ class Renderer { return replace( mapOf( "(.+)<\\/a>" to "\\2<\\/a>", - "\\\"\\%5E([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})(\\/?.*\\\")" - to "\"..\\/\\1\\/rendered\\2" + "\\\"\\%5E([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})(\\/?.*\\\")" to "\"..\\/\\1\\/rendered\\2" ), filenameProvider ) @@ -203,7 +167,12 @@ class Renderer { } } - fun render(document: Document, fsService: FileSystemService, cache: RenderCache): RenderedDocument? { + fun render( + document: Document, + fsService: FileSystemService, + cache: RenderCache, + useCache: Boolean = true + ): RenderedDocument? { return render( RenderMeta( document, @@ -212,12 +181,16 @@ class Renderer { document.id.toString() + if (document.fileExtension != null) "." + document.fileExtension else "", cache, ), - cache + cache, useCache ) } - fun render(meta: RenderMeta, cache: RenderCache): RenderedDocument? { - if (cache.isCacheValid(meta.document.id)) return cache.getCachedRender(meta.document.id) + private fun render(meta: RenderMeta, cache: RenderCache, useCache: Boolean = true): RenderedDocument? { + logger.debug("Rendering using renderer $name") + if (useCache) { + if (cache.isCacheValid(meta.document.id)) return cache.getCachedRender(meta.document.id) + logger.trace("document cache is invalid or nonexistent, rendering") + } for (step in renderSteps) step(meta) for (step in meta.cleanup) step(meta) return cache.cacheRender(meta.document.id, meta.output!!) diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/RenderingException.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderingException.kt similarity index 52% rename from backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/RenderingException.kt rename to backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderingException.kt index dce7d2c..b388f2c 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/RenderingException.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/renderer/RenderingException.kt @@ -1,3 +1,3 @@ -package me.kruemmelspalter.file_spider.backend +package me.kruemmelspalter.file_spider.backend.renderer class RenderingException(msg: String) : RuntimeException(msg) diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentMeta.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentMeta.kt index ef71fb0..1d0c0e8 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentMeta.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentMeta.kt @@ -1,6 +1,6 @@ package me.kruemmelspalter.file_spider.backend.services -import me.kruemmelspalter.file_spider.backend.database.model.Document +import me.kruemmelspalter.file_spider.backend.database.Document import java.sql.Timestamp import java.util.UUID diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentService.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentService.kt index 46ddbb2..d0933e8 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentService.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/DocumentService.kt @@ -1,10 +1,9 @@ package me.kruemmelspalter.file_spider.backend.services import com.fasterxml.uuid.Generators -import me.kruemmelspalter.file_spider.backend.database.dao.DocumentRepository -import me.kruemmelspalter.file_spider.backend.database.model.Document -import me.kruemmelspalter.file_spider.backend.renderer.RenderCache -import me.kruemmelspalter.file_spider.backend.renderer.Renderer +import me.kruemmelspalter.file_spider.backend.database.Document +import me.kruemmelspalter.file_spider.backend.database.DocumentRepository +import me.kruemmelspalter.file_spider.backend.renderer.RenderService import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.io.Resource import org.springframework.stereotype.Service @@ -16,50 +15,49 @@ import javax.servlet.ServletContext @Service class DocumentService { - private val uuidGenerator = Generators.timeBasedGenerator()!! + private val uuidGenerator = Generators.timeBasedGenerator() @Autowired - val documentRepository: DocumentRepository? = null + private lateinit var documentRepository: DocumentRepository @Autowired - val fsService: FileSystemService? = null + private lateinit var renderService: RenderService @Autowired - private lateinit var renderCache: RenderCache + private lateinit var fsService: FileSystemService fun getDocumentMeta(id: UUID): DocumentMeta? { - val document = documentRepository!!.getDocument(id) + val document = documentRepository.getDocument(id) return documentToMeta(document ?: return null) } private fun documentToMeta(document: Document): DocumentMeta { - val attributes = fsService!!.getFileAttributesFromID(document.id, document.fileExtension) + val attributes = fsService.getFileAttributesFromID(document.id, document.fileExtension) return DocumentMeta( document, Timestamp(attributes.creationTime().toMillis()), Timestamp(attributes.lastModifiedTime().toMillis()), Timestamp(attributes.lastAccessTime().toMillis()), - documentRepository!!.getTags(document.id) + documentRepository.getTags(document.id) ) } fun filterDocuments(posFilter: List, negFilter: List): List { - return documentRepository!!.filterDocuments(posFilter, negFilter).map { documentToMeta(it) } + return documentRepository.filterDocuments(posFilter, negFilter).map { documentToMeta(it) } } - fun renderDocument(id: UUID): RenderedDocument? { - val document = documentRepository!!.getDocument(id) - return if (document == null) null else Renderer.getRenderer(document.renderer).render(document, fsService!!, renderCache) + fun renderDocument(id: UUID, useCache: Boolean = true): RenderedDocument? { + return renderService.renderDocument(documentRepository.getDocument(id) ?: return null, useCache) } fun readDocumentLog(id: UUID): InputStream? { - documentRepository!!.getDocument(id) ?: return null + documentRepository.getDocument(id) ?: return null - return fsService!!.readLog(id) + return fsService.readLog(id) } fun setTitle(id: UUID, title: String) { - documentRepository!!.setTitle(id, title) + documentRepository.setTitle(id, title) } fun createDocument( @@ -73,7 +71,7 @@ class DocumentService { ): UUID { val uuid = uuidGenerator.generate() - documentRepository!!.createDocument( + documentRepository.createDocument( uuid, title ?: "Untitled", renderer ?: "plain", @@ -83,9 +81,9 @@ class DocumentService { fileExtension, ) - fsService!!.createDocument(uuid, fileExtension) + fsService.createDocument(uuid, fileExtension) if (content != null) { - fsService!!.writeToDocument(uuid, fileExtension, content) + fsService.writeToDocument(uuid, fileExtension, content) content.close() } @@ -93,19 +91,19 @@ class DocumentService { } fun addTags(id: UUID, addTags: List) { - documentRepository!!.addTags(id, addTags) + documentRepository.addTags(id, addTags) } fun removeTags(id: UUID, removeTags: List) { - documentRepository!!.removeTags(id, removeTags) + documentRepository.removeTags(id, removeTags) } fun deleteDocument(id: UUID) { - documentRepository!!.deleteDocument(id) - fsService!!.deleteDocument(id) + documentRepository.deleteDocument(id) + fsService.deleteDocument(id) } fun getDocumentResource(id: UUID, fileName: String, servletContext: ServletContext): Resource? { - return fsService!!.getDocumentResource(id, fileName, servletContext) + return fsService.getDocumentResource(id, fileName, servletContext) } } diff --git a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/FileSystemService.kt b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/FileSystemService.kt index fd702cb..7b95335 100644 --- a/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/FileSystemService.kt +++ b/backend/app/src/main/kotlin/me/kruemmelspalter/file_spider/backend/services/FileSystemService.kt @@ -8,11 +8,9 @@ import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.util.FileSystemUtils import org.springframework.web.server.ResponseStatusException -import java.io.BufferedReader import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -import java.io.FileReader import java.io.InputStream import java.nio.file.Files import java.nio.file.Path @@ -60,23 +58,6 @@ class FileSystemService { return getDocumentPathFromID(id, fileExtension).toFile() } - fun getInputStreamFromID(id: UUID, fileExtension: String?): InputStream { - return FileInputStream(getFileFromID(id, fileExtension)) - } - - fun getContentFromID(id: UUID, fileExtension: String?): String { - val resultBuilder = StringBuilder() - val br = BufferedReader(FileReader(getFileFromID(id, fileExtension))) - br.use { - var line = br.readLine() - while (line != null) { - resultBuilder.append(line).append("\n") - line = br.readLine() - } - } - return resultBuilder.toString() - } - fun getFileAttributesFromID(id: UUID, fileExtension: String?): BasicFileAttributes { return Files.readAttributes(getDocumentPathFromID(id, fileExtension), BasicFileAttributes::class.java) } diff --git a/backend/app/src/main/resources/sql/init.sql b/backend/app/src/main/resources/sql/init.sql index 8b3bf1b..213e47d 100644 --- a/backend/app/src/main/resources/sql/init.sql +++ b/backend/app/src/main/resources/sql/init.sql @@ -11,7 +11,7 @@ create table Document ( create table Tag ( document uuid not null, - tag varchar(20) not null, + tag varchar(50) not null, primary key (document, tag) ); @@ -21,4 +21,4 @@ create table Cache ( mimeType varchar(64) not null, fileName varchar(64) not null, primary key (document) -) \ No newline at end of file +) diff --git a/docker-compose.yml b/docker-compose.yml index cf84601..0e64cf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,15 @@ services: backend: image: ghcr.io/kruemmelspalter/filespider-backend:${FILESPIDER_TAG} environment: - APP_OPTS: -Ddatabase.host=database -Ddatabase.username=user -Ddatabase.password=password -Ddatabase.database=database + APP_OPTS: | + -Ddatabase.host=database + -Ddatabase.username=user + -Ddatabase.password=password + -Ddatabase.database=database + -Dlogging.level.org.apache.tomcat.util.net.NioEndpoint=INFO + -Dlogging.level.org.apache.catalina.session.ManagerBase=INFO + -Dlogging.level.org.apache.coyote.http11.Http11Processor=INFO + -Dlogging.level.root=INFO # change according to needs volumes: - ./data/filespider:/var/lib/filespider depends_on: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..cc1629e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,90 @@ +# Created by .ignore support plugin (hsz.mobi) +### Node template +# Logs +/logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +# .cache + +# next.js build output +# .next + +# nuxt.js build output +# .nuxt + +# Nuxt generate +# dist + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# IDE / Editor +.idea + +# Service worker +sw.* + +# macOS +.DS_Store + +# Vim swap files +*.swp diff --git a/frontend/components/DocumentDisplay.vue b/frontend/components/DocumentDisplay.vue index 7559ff1..606f859 100644 --- a/frontend/components/DocumentDisplay.vue +++ b/frontend/components/DocumentDisplay.vue @@ -156,7 +156,7 @@ export default { #document-content { width: 100%; height: 100%; - min-height: 80vh; + min-height: 78.3vh; border: none; } diff --git a/frontend/package.json b/frontend/package.json index 5640b57..aaa37f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "NODE_OPTIONS=--openssl-legacy-provider nuxt", + "dev": "nuxt", "build": "NODE_OPTIONS=--openssl-legacy-provider nuxt build", "start": "NODE_OPTIONS=--openssl-legacy-provider nuxt start", "generate": "NODE_OPTIONS=--openssl-legacy-provider nuxt generate",