-
Notifications
You must be signed in to change notification settings - Fork 4
SED-4816 Introduce AttachmentStorage #676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cl-exense
wants to merge
7
commits into
master
Choose a base branch
from
SED-4816-refactor-attachments-resources
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
1c33205
SED-4816 Introduce AttachmentStorage
cl-exense cea617b
SED-4816 Cleanup
cl-exense 1a63776
Merge branch 'master' into SED-4816-refactor-attachments-resources
cl-exense da01686
SED-4816 Robot PR feedback
cl-exense b2ddea7
Merge branch 'master' into SED-4816-refactor-attachments-resources
cl-exense d863264
SED-4816 Minor enhancements
cl-exense ac06d1f
SED-4816 Add auto-discovery of MIME types by extension
cl-exense File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,41 @@ | ||
| package step.plugins.streaming; | ||
|
|
||
| import org.bson.types.ObjectId; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import step.attachments.AttachmentMeta; | ||
| import step.attachments.SkippedAttachmentMeta; | ||
| import step.attachments.StreamingAttachmentMeta; | ||
| import step.constants.LiveReportingConstants; | ||
| import step.core.GlobalContext; | ||
| import step.core.access.User; | ||
| import step.core.deployment.AuthorizationException; | ||
| import step.core.execution.ExecutionContext; | ||
| import step.core.objectenricher.ObjectEnricher; | ||
| import step.core.objectenricher.ObjectHookRegistry; | ||
| import step.framework.server.Session; | ||
| import step.framework.server.access.AuthorizationManager; | ||
| import step.resources.StreamingResourceContentProvider; | ||
| import step.streaming.common.*; | ||
| import step.plugins.streaming.util.MimeTypeUtil; | ||
| import step.resources.AttachmentStorage; | ||
| import step.streaming.common.QuotaExceededException; | ||
| import step.streaming.common.StreamingResourceMetadata; | ||
| import step.streaming.common.StreamingResourceReference; | ||
| import step.streaming.common.StreamingResourceUploadContext; | ||
| import step.streaming.common.StreamingResourceUploadContexts; | ||
| import step.streaming.server.DefaultStreamingResourceManager; | ||
| import step.streaming.server.StreamingResourcesStorageBackend; | ||
|
|
||
| import java.io.ByteArrayInputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.util.Objects; | ||
| import java.util.function.Function; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class StepStreamingResourceManager extends DefaultStreamingResourceManager implements StreamingResourceContentProvider { | ||
| public class StepStreamingResourceManager extends DefaultStreamingResourceManager implements AttachmentStorage { | ||
| private static final Logger logger = LoggerFactory.getLogger(StepStreamingResourceManager.class); | ||
| static final String ATTRIBUTE_STEP_SESSION = "stepSession"; | ||
| private static final Pattern MIME_TYPE_PATTERN = Pattern.compile("^[a-zA-Z0-9!#$&^_.+-]+/[a-zA-Z0-9!#$&^_.+-]+$"); | ||
|
|
||
|
|
||
| private final AuthorizationManager<User, Session<User>> authorizationManager; | ||
|
|
@@ -139,8 +154,84 @@ public void checkDownloadPermission(StreamingResource resource, Session<User> st | |
| } | ||
| } | ||
|
|
||
| /* The following methods implement the AttachmentStorage interface */ | ||
|
|
||
| @Override | ||
| public InputStream getResourceContentStream(String resourceId) throws Exception { | ||
| public InputStream getAttachmentStream(String resourceId) throws Exception { | ||
| return openStream(resourceId, 0, getStatus(resourceId).getCurrentSize()); | ||
| } | ||
|
|
||
| @Override | ||
| public AttachmentMeta saveAttachment(Object executionContext, byte[] content, String filename, String mimeType) { | ||
| if (!(executionContext instanceof ExecutionContext context)) { | ||
| String className = executionContext == null ? "null" : executionContext.getClass().getName(); | ||
| return new SkippedAttachmentMeta(filename, mimeType, "UNEXPECTED: Invalid execution context type of class " + className); | ||
| } | ||
| // Java 16+ Voodoo: the (negative) instanceof check above actually defines the "context" variable and keeps it in scope here. :-) | ||
| // This is intentional; the reasoning is to keep the "happy path" straightforward, by allowing for that kind of guard clauses with early returns. | ||
| StreamingResourceUploadContexts uploadContexts = context.get(StreamingResourceUploadContexts.class); | ||
|
|
||
| if (uploadContexts == null) { | ||
| return new SkippedAttachmentMeta(filename, mimeType, "UNEXPECTED: no StreamingResourceUploadContexts found in execution context"); | ||
| } | ||
|
|
||
| // We don't need to react to any events, but we need an UploadContext to convey relevant contextual information about the attachment. | ||
| StreamingResourceUploadContext uploadContext = new StreamingResourceUploadContext(); | ||
| ObjectEnricher enricher = context.getObjectEnricher(); | ||
| if (enricher != null) { | ||
| uploadContext.getAttributes().put(LiveReportingConstants.ACCESSCONTROL_ENRICHER, enricher); | ||
| } | ||
| uploadContext.getAttributes().put(LiveReportingConstants.CONTEXT_EXECUTION_ID, context.getExecutionId()); | ||
| uploadContext.getAttributes().put(LiveReportingConstants.CONTEXT_VARIABLES_MANAGER, context.getVariablesManager()); | ||
| uploadContext.getAttributes().put(LiveReportingConstants.CONTEXT_REPORT_NODE, context.getCurrentReportNode()); | ||
| uploadContexts.registerContext(uploadContext); | ||
|
|
||
| try { | ||
| return createAttachmentFromContent(content, filename, mimeType, uploadContext.contextId); | ||
| } catch (Exception e) { | ||
| return new SkippedAttachmentMeta(filename, mimeType, e.getMessage()); | ||
| } finally { | ||
| uploadContexts.unregisterContext(uploadContext); | ||
| } | ||
| } | ||
|
|
||
| public AttachmentMeta createAttachmentFromContent(byte[] content, String filename, String mimeType, String uploadContextId) throws QuotaExceededException, IOException { | ||
| Objects.requireNonNull(filename, "filename must not be null"); | ||
| if (mimeType == null) { | ||
| // Try to determine from filename; remains null for unknown extensions | ||
| mimeType = MimeTypeUtil.getMimeTypeForFilename(filename); | ||
| } | ||
| mimeType = sanitizeMimeType(mimeType); | ||
| boolean supportsLineAccess = isLineAccessSupported(mimeType); | ||
| StreamingResourceMetadata metadata = new StreamingResourceMetadata(filename, mimeType, supportsLineAccess); | ||
| String resourceId = registerNewResource(metadata, uploadContextId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not related to this PR and should definitely not be refactored as part of it, but we should have avoided to use the word "resource" in the context of streaming as it's quite confusing with the legacy Step "resources" |
||
| StreamingAttachmentMeta meta = new StreamingAttachmentMeta(new ObjectId(resourceId), filename, mimeType); | ||
| // Saving will be completely finished, in a single call, because all data is already present from the beginning. | ||
| writeChunk(resourceId, new ByteArrayInputStream(content), true); | ||
| var status = markCompleted(resourceId); | ||
| meta.setCurrentNumberOfLines(status.getNumberOfLines()); | ||
| meta.setCurrentSize(status.getCurrentSize()); | ||
| meta.setStatus(StreamingAttachmentMeta.Status.COMPLETED); | ||
| return meta; | ||
| } | ||
|
|
||
| private static String sanitizeMimeType(String mimeType) { | ||
| if (mimeType == null) { | ||
| // fallback if no mimeType was given | ||
| return "application/octet-stream"; | ||
| } | ||
| mimeType = mimeType.trim(); // just in case | ||
| if (!MIME_TYPE_PATTERN.matcher(mimeType).matches()) { | ||
| logger.warn("Invalid mime type \"{}\", replacing with generic application/octet-stream", mimeType); | ||
| return "application/octet-stream"; | ||
| } | ||
| return mimeType; | ||
| } | ||
|
|
||
| private static boolean isLineAccessSupported(String mimeType) { | ||
| // Very simple heuristic, but should catch almost all useful cases | ||
| return mimeType.startsWith("text/") | ||
| || mimeType.equals("application/xml") | ||
| || mimeType.equals("application/json"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
.../step-controller-base-plugins/src/main/java/step/plugins/streaming/util/MimeTypeUtil.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package step.plugins.streaming.util; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
|
|
||
| public class MimeTypeUtil { | ||
| private MimeTypeUtil() { | ||
| } | ||
|
|
||
| private static final Map<String, String> mimeTypeForExtension = makeMap(); | ||
|
|
||
| private static Map<String, String> makeMap() { | ||
| Map<String, String> map = new HashMap<>(); | ||
|
|
||
| // Text & Code | ||
| List.of("txt", "log", "ini", "conf", "java", "c", "cpp", "h", "hpp", "sh", "bat", "ps1", "sql", "php", "rb", "pl", "go", "rs") | ||
| .forEach(m -> map.put(m, "text/plain")); | ||
| List.of("md", "markdown").forEach(m -> map.put(m, "text/markdown")); | ||
| map.put("csv", "text/csv"); | ||
| List.of("htm", "html").forEach(m -> map.put(m, "text/html")); | ||
|
|
||
| // Application Data | ||
| map.put("json", "application/json"); | ||
| map.put("xml", "application/xml"); | ||
| List.of("yaml", "yml").forEach(m -> map.put(m, "application/yaml")); | ||
| map.put("pdf", "application/pdf"); | ||
|
|
||
| // Images | ||
| List.of("jpg", "jpeg").forEach(m -> map.put(m, "image/jpeg")); | ||
| map.put("png", "image/png"); | ||
| map.put("gif", "image/gif"); | ||
| map.put("bmp", "image/bmp"); | ||
| map.put("webp", "image/webp"); | ||
| List.of("tiff", "tif").forEach(m -> map.put(m, "image/tiff")); | ||
| map.put("svg", "image/svg+xml"); | ||
| map.put("heic", "image/heic"); | ||
| map.put("avif", "image/avif"); | ||
|
|
||
| // Video | ||
| List.of("mp4", "m4v").forEach(m -> map.put(m, "video/mp4")); | ||
| map.put("mkv", "video/x-matroska"); | ||
| map.put("mov", "video/quicktime"); | ||
| map.put("avi", "video/x-msvideo"); | ||
| map.put("wmv", "video/x-ms-vwm"); | ||
| map.put("flw", "video/x-flv"); | ||
| map.put("webm", "video/webm"); | ||
| map.put("mpeg", "video/mpeg"); | ||
| map.put("mpg", "video/mpg"); | ||
| map.put("3gp", "video/3gpp"); | ||
| map.put("3g2", "video/3gpp2"); | ||
| map.put("ogv", "video/ogg"); | ||
| return map; | ||
| } | ||
|
|
||
| public static String getMimeTypeForFilename(String filename) { | ||
| if (filename == null) { | ||
| // unexpected, but let's be prudent | ||
| return null; | ||
| } | ||
| int dot = filename.lastIndexOf('.'); | ||
| if (dot == -1) { | ||
| // no extension | ||
| return null; | ||
| } | ||
| String extension = filename.substring(dot + 1).toLowerCase(Locale.ROOT); | ||
| return mimeTypeForExtension.get(extension); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.