Skip to content

Commit

Permalink
Merge pull request #219 from code-freak/feature/arbitrary-upload
Browse files Browse the repository at this point in the history
Support non-archive uploads and other archive types
  • Loading branch information
erikhofer committed Oct 7, 2019
2 parents 60ba4bc + 678d499 commit 819e135
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 34 deletions.
Expand Up @@ -103,7 +103,7 @@ class AssignmentController : BaseController() {
val deadline = parseLocalDateTime(deadlineString, "deadline")

ByteArrayOutputStream().use { out ->
TarUtil.processUploadedArchive(file, out)
TarUtil.writeUploadAsTar(file, out)
val result = assignmentService.createFromTar(out.toByteArray(), user.entity, deadline)
model.successMessage("Assignment has been created.")
if (result.taskErrors.isNotEmpty()) {
Expand Down
Expand Up @@ -96,7 +96,7 @@ class TaskController : BaseController() {
): String {
val submission = getOrCreateSubmissionForTask(taskId)
val answer = submission.getOrCreateAnswer(taskId)
answerService.setFiles(answer).use { TarUtil.processUploadedArchive(file, it) }
answerService.setFiles(answer).use { TarUtil.writeUploadAsTar(file, it) }
model.successMessage("Successfully uploaded source for task '${answer.task.title}'.")
return "redirect:" + urls.get(submission.assignment)
}
Expand Down Expand Up @@ -153,7 +153,7 @@ class TaskController : BaseController() {
) = withErrorPage("/import") {

ByteArrayOutputStream().use { out ->
TarUtil.processUploadedArchive(file, out)
TarUtil.writeUploadAsTar(file, out)
val task = taskService.updateFromTar(out.toByteArray(), taskId)
model.successMessage("Task '${task.title}' has been updated.")
"redirect:" + urls.get(task.assignment)
Expand Down
59 changes: 35 additions & 24 deletions src/main/kotlin/de/code_freak/codefreak/util/TarUtil.kt
Expand Up @@ -2,12 +2,15 @@ package de.code_freak.codefreak.util

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import org.apache.commons.compress.archivers.ArchiveException
import org.apache.commons.compress.archivers.ArchiveStreamFactory
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import org.apache.commons.compress.compressors.CompressorException
import org.apache.commons.compress.compressors.CompressorStreamFactory
import org.apache.commons.compress.utils.IOUtils
import org.springframework.util.StreamUtils
import org.springframework.web.multipart.MultipartFile
Expand Down Expand Up @@ -37,12 +40,6 @@ object TarUtil {
}
}

@Throws(IOException::class)
fun checkValidTar(`in`: InputStream) {
val tar = TarArchiveInputStream(`in`)
generateSequence { tar.nextTarEntry }.forEach { _ -> /** Do nothing, just throw on error. */ }
}

fun createTarFromDirectory(file: File, out: OutputStream) {
require(file.isDirectory) { "FileCollection must be a directory" }

Expand Down Expand Up @@ -93,15 +90,22 @@ object TarUtil {
zip.finish()
}

fun zipToTar(`in`: InputStream, out: OutputStream) {
val zip = ZipArchiveInputStream(`in`)
fun archiveToTar(`in`: InputStream, out: OutputStream) {
var input = BufferedInputStream(`in`)
try {
// try to read input as compressed type
input = BufferedInputStream(CompressorStreamFactory().createCompressorInputStream(input))
} catch (e: CompressorException) {
// input is not compressed or maybe even not an archive at all
}
val archive = ArchiveStreamFactory().createArchiveInputStream(input)
val tar = TarArchiveOutputStream(out)
generateSequence { zip.nextZipEntry }.forEach { zipEntry ->
val tarEntry = TarArchiveEntry(normalizeEntryName(zipEntry.name))
if (zipEntry.isDirectory) {
generateSequence { archive.nextEntry }.forEach { archiveEntry ->
val tarEntry = TarArchiveEntry(normalizeEntryName(archiveEntry.name))
if (archiveEntry.isDirectory) {
tar.putArchiveEntry(tarEntry)
} else {
val content = zip.readBytes()
val content = archive.readBytes()
tarEntry.size = content.size.toLong()
tar.putArchiveEntry(tarEntry)
tar.write(content)
Expand Down Expand Up @@ -153,21 +157,28 @@ object TarUtil {
}
}

fun processUploadedArchive(file: MultipartFile, out: OutputStream) {
val filename = file.originalFilename ?: ""
fun writeUploadAsTar(file: MultipartFile, out: OutputStream) {
try {
when {
filename.endsWith(".tar", true) -> {
file.inputStream.use { checkValidTar(it) }
file.inputStream.use { StreamUtils.copy(it, out) }
}
filename.endsWith(".zip", true) -> {
file.inputStream.use { zipToTar(it, out) }
}
else -> throw IllegalArgumentException("Unsupported file format")
try {
// try to read upload as archive
file.inputStream.use { archiveToTar(it, out) }
} catch (e: ArchiveException) {
// unknown archive type or no archive at all
// create a new tar archive that contains only the uploaded file
wrapUploadInTar(file, out)
}
} catch (e: IOException) {
throw IllegalArgumentException("File could not be processed")
}
}

private fun wrapUploadInTar(file: MultipartFile, out: OutputStream) {
val outputStream = TarArchiveOutputStream(out)
val entry = TarArchiveEntry(file.originalFilename)
entry.size = file.size
outputStream.putArchiveEntry(entry)
file.inputStream.use { StreamUtils.copy(it, outputStream) }
outputStream.closeArchiveEntry()
outputStream.finish()
}
}
2 changes: 1 addition & 1 deletion src/main/resources/templates/assignment.html
Expand Up @@ -84,7 +84,7 @@ <h4 class="modal-title" th:text='|Upload/Import files for "${task.title}"|'/>
<div class="modal-body">
<form class="mb-4" th:action="@{${@urls.get(task)} + '/source'}" method="post" enctype="multipart/form-data">
<h5>Upload files from computer</h5>
<th:block th:replace="fragments/archive-upload" th:with="inputId=${'task-file-upload-' + task.position}"></th:block>
<th:block th:replace="fragments/file-upload" th:with="inputId=${'task-file-upload-' + task.position}"></th:block>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
<div th:if="${#lists.size(supportedGitRemotes)}">
Expand Down
Expand Up @@ -5,20 +5,21 @@
<div class="col">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">Archive:</span>
<span class="input-group-text">File/Archive:</span>
</div>
<div class="custom-file">
<input type="file" required name="file" class="custom-file-input" th:id="${inputId}" accept=".tar,.zip">
<label class="custom-file-label" th:for="${inputId}">Please select an archive…</label>
<input type="file" required name="file" class="custom-file-input" th:id="${inputId}">
<label class="custom-file-label"
th:for="${inputId}">Please select a single file or an archive of multiple files…</label>
</div>
</div>
</div>
<div class="col-auto">
<button class="btn btn-primary"><i class="fas fa-upload"></i> Upload Archive</button>
<button class="btn btn-primary"><i class="fas fa-upload"></i> Upload</button>
</div>
</div>
<div class="form-text text-muted small">
Supported formats: .tar, .zip
If you want to upload multiple files please create a .zip, .tar or tar.gz archive.
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion src/main/resources/templates/import.html
Expand Up @@ -13,7 +13,7 @@ <h3 class="card-title">Create / update assignment</h3>
<label>Deadline (optional)</label>
<input class="form-control" type="datetime-local" name="deadline"/>
</div>
<div th:replace="fragments/archive-upload" th:with="inputId='assignment-file-upload'"></div>
<div th:replace="fragments/file-upload" th:with="inputId='assignment-file-upload'"></div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
</div>
Expand Down

0 comments on commit 819e135

Please sign in to comment.