exceptionfactory commented on code in PR #9138:
URL: https://github.com/apache/nifi/pull/9138#discussion_r1704502027


##########
nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/file/FileUtils.java:
##########
@@ -582,4 +582,39 @@ public static long getContainerUsableSpace(final Path 
path) {
         return path.toFile().getUsableSpace();
     }
 
+    // The invalid character list is derived from this Stackoverflow page.
+    // 
https://stackoverflow.com/questions/1155107/is-there-a-cross-platform-java-method-to-remove-filename-special-chars
+    private final static int[] INVALID_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+            16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 
58, 42, 63, 92, 47, 32};
+
+    static {
+        Arrays.sort(INVALID_CHARS);
+    }
+
+    /**
+     * Replaces invalid characters for a file system name within a given 
filename string to underscore '_'.
+     * Be careful not to pass a file path as this method replaces path 
delimiter characters (i.e forward/back slashes).
+     * @param filename The filename to clean
+     * @return sanitized filename
+     */
+    public static String sanitizeFilename(String filename) {

Review Comment:
   Minor naming recommendation: 
   ```suggestion
       public static String getSanitizedFilename(String filename) {
   ```



##########
nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/FlowControllerConfiguration.java:
##########
@@ -434,4 +439,24 @@ public NarManager narManager(@Autowired final 
NarPersistenceProvider narPersiste
                 properties
         );
     }
+
+    /**
+     * Asset Manager from Flow Controller
+     *
+     * @return Asset Manager
+     */
+    @Bean
+    public AssetManager assetManager(@Autowired final FlowController 
flowController) {

Review Comment:
   Instead of wiring the `FlowController` as an argument, it should be possible 
to call `flowController()`.



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java:
##########
@@ -1509,6 +1512,26 @@ private ParameterProviderConfigurationEntity 
createParameterProviderConfiguratio
        return config;
    }
 
+   public AssetEntity createAssetEntity(final Asset asset) {
+         final AssetEntity entity = new AssetEntity();
+         entity.setAsset(createAssetDto(asset));
+         return entity;
+   }
+
+   public AssetDTO createAssetDto(final Asset asset) {
+       final File assetFile = asset.getFile();
+       final AssetDTO dto = new AssetDTO();
+       dto.setId(asset.getIdentifier());
+       dto.setName(asset.getName());
+       dto.setDigest(asset.getDigest().orElse(null));
+       dto.setMissingContent(!assetFile.exists());

Review Comment:
   What do you think about naming this property `contentNotFound` instead of 
`missingContent`?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterContextResource.java:
##########
@@ -323,6 +356,204 @@ public Response updateParameterContext(
         );
     }
 
+    @POST
+    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{contextId}/assets")
+    @Operation(
+            summary = "Creates a new Asset in the given Parameter Context",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = AssetEntity.class))),
+            description = "This endpoint will create a new Asset in the given 
Parameter Context. The Asset will be created with the given name and the 
contents of the file that is uploaded. " +
+                    "The Asset will be created in the given Parameter Context, 
and will be available for use by any component that references the Parameter 
Context.",
+            security = {
+                @SecurityRequirement(name = "Read - 
/parameter-contexts/{parameterContextId}"),
+                @SecurityRequirement(name = "Write - 
/parameter-contexts/{parameterContextId}"),
+                @SecurityRequirement(name = "Read - for every component that 
is affected by the update"),
+                @SecurityRequirement(name = "Write - for every component that 
is affected by the update"),
+                @SecurityRequirement(name = "Read - for every currently 
inherited parameter context")
+            }
+    )
+    @ApiResponses(
+        value = {
+            @ApiResponse(responseCode = "400", description = "NiFi was unable 
to complete the request because it was invalid. The request should not be 
retried without modification."),
+            @ApiResponse(responseCode = "401", description = "Client could not 
be authenticated."),
+            @ApiResponse(responseCode = "403", description = "Client is not 
authorized to make this request."),
+            @ApiResponse(responseCode = "404", description = "The specified 
resource could not be found."),
+            @ApiResponse(responseCode = "409", description = "The request was 
valid but NiFi was not in the appropriate state to process it.")
+        }
+    )
+    public Response createAsset(
+            @PathParam("contextId") final String contextId,
+            @HeaderParam(FILENAME_HEADER) final String assetName,
+            @Parameter(description = "The contents of the asset.", required = 
true) final InputStream assetContents) throws IOException {
+
+        // Validate input
+        if (StringUtils.isBlank(assetName)) {
+            throw new IllegalArgumentException(FILENAME_HEADER + " header is 
required");
+        }
+        if (assetContents == null) {
+            throw new IllegalArgumentException("Asset contents must be 
specified.");
+        }
+
+        // If clustered and not all nodes are connected, do not allow creating 
an asset.
+        // Generally, we allow the flow to be modified when nodes are 
disconnected, but we do not allow creating an asset because
+        // the cluster has no mechanism for synchronizing those assets after 
the upload.
+        final ClusterCoordinator clusterCoordinator = getClusterCoordinator();
+        if (clusterCoordinator != null) {
+            final Set<NodeIdentifier> disconnectedNodes = 
clusterCoordinator.getNodeIdentifiers(NodeConnectionState.CONNECTING, 
NodeConnectionState.DISCONNECTED, NodeConnectionState.DISCONNECTING);
+            if (!disconnectedNodes.isEmpty()) {
+                throw new IllegalStateException("Cannot create an Asset 
because the following %s nodes are not currently connected: 
%s".formatted(disconnectedNodes.size(), disconnectedNodes));
+            }
+        }
+
+        // Get the context or throw ResourceNotFoundException
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final ParameterContextEntity contextEntity = 
serviceFacade.getParameterContext(contextId, false, user);
+        final Set<AffectedComponentEntity> affectedComponents = 
serviceFacade.getComponentsAffectedByParameterContextUpdate(Collections.singletonList(contextEntity.getComponent()));
+
+        // Authorize the request
+        serviceFacade.authorizeAccess(lookup -> {
+            // Verify READ and WRITE permissions for user, for the Parameter 
Context itself
+            final ParameterContext parameterContext = 
lookup.getParameterContext(contextId);
+            parameterContext.authorize(authorizer, RequestAction.READ, user);
+            parameterContext.authorize(authorizer, RequestAction.WRITE, user);
+
+            // Verify READ and WRITE permissions for user, for every component 
that is affected
+            affectedComponents.forEach(component -> 
parameterUpdateManager.authorizeAffectedComponent(component, lookup, user, 
true, true));
+        });
+
+        // If we need to replicate the request, we do so using the Upload 
Request Replicator, rather than the typical replicate() method.
+        // This is because Upload Request Replication works differently in 
that it needs to be able to replicate the InputStream multiple times,
+        // so it must create a file on disk to do so and then use the file's 
content to replicate the request. It also bypasses the two-phase
+        // commit process that is used for other requests because doing so 
would result in uploading the file twice to each node or providing a
+        // different request for each of the two phases.
+
+        final long startTime = System.currentTimeMillis();
+        final InputStream maxLengthInputStream = new 
MaxLengthInputStream(assetContents, (long) DataUnit.GB.toB(1));
+
+        final AssetEntity assetEntity;
+        if (isReplicateRequest()) {
+            final UploadRequest<AssetEntity> uploadRequest = new 
UploadRequest.Builder<AssetEntity>()
+                    .user(NiFiUserUtils.getNiFiUser())
+                    .filename(assetName)
+                    .identifier(UUID.randomUUID().toString())
+                    .contents(maxLengthInputStream)
+                    .header(FILENAME_HEADER, assetName)
+                    .header(CONTENT_TYPE_HEADER, UPLOAD_CONTENT_TYPE)
+                    .exampleRequestUri(getAbsolutePath())
+                    .responseClass(AssetEntity.class)
+                    .successfulResponseStatus(HttpResponseStatus.OK.getCode())
+                    .build();
+            assetEntity = uploadRequestReplicator.upload(uploadRequest);
+        } else {
+            final String existingContextId = contextEntity.getId();
+            final String sanitizedAssetName = 
FileUtils.sanitizeFilename(assetName);

Review Comment:
   Instead of simply sanitizing the filename, should an input filename be 
rejected if it contains unexpected characters?



##########
nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterContextResource.java:
##########
@@ -323,6 +356,204 @@ public Response updateParameterContext(
         );
     }
 
+    @POST
+    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{contextId}/assets")
+    @Operation(
+            summary = "Creates a new Asset in the given Parameter Context",
+            responses = @ApiResponse(content = @Content(schema = 
@Schema(implementation = AssetEntity.class))),
+            description = "This endpoint will create a new Asset in the given 
Parameter Context. The Asset will be created with the given name and the 
contents of the file that is uploaded. " +
+                    "The Asset will be created in the given Parameter Context, 
and will be available for use by any component that references the Parameter 
Context.",
+            security = {
+                @SecurityRequirement(name = "Read - 
/parameter-contexts/{parameterContextId}"),
+                @SecurityRequirement(name = "Write - 
/parameter-contexts/{parameterContextId}"),
+                @SecurityRequirement(name = "Read - for every component that 
is affected by the update"),
+                @SecurityRequirement(name = "Write - for every component that 
is affected by the update"),
+                @SecurityRequirement(name = "Read - for every currently 
inherited parameter context")
+            }
+    )
+    @ApiResponses(
+        value = {
+            @ApiResponse(responseCode = "400", description = "NiFi was unable 
to complete the request because it was invalid. The request should not be 
retried without modification."),
+            @ApiResponse(responseCode = "401", description = "Client could not 
be authenticated."),
+            @ApiResponse(responseCode = "403", description = "Client is not 
authorized to make this request."),
+            @ApiResponse(responseCode = "404", description = "The specified 
resource could not be found."),
+            @ApiResponse(responseCode = "409", description = "The request was 
valid but NiFi was not in the appropriate state to process it.")
+        }
+    )
+    public Response createAsset(
+            @PathParam("contextId") final String contextId,
+            @HeaderParam(FILENAME_HEADER) final String assetName,
+            @Parameter(description = "The contents of the asset.", required = 
true) final InputStream assetContents) throws IOException {
+
+        // Validate input
+        if (StringUtils.isBlank(assetName)) {
+            throw new IllegalArgumentException(FILENAME_HEADER + " header is 
required");
+        }
+        if (assetContents == null) {
+            throw new IllegalArgumentException("Asset contents must be 
specified.");
+        }
+
+        // If clustered and not all nodes are connected, do not allow creating 
an asset.
+        // Generally, we allow the flow to be modified when nodes are 
disconnected, but we do not allow creating an asset because
+        // the cluster has no mechanism for synchronizing those assets after 
the upload.
+        final ClusterCoordinator clusterCoordinator = getClusterCoordinator();
+        if (clusterCoordinator != null) {
+            final Set<NodeIdentifier> disconnectedNodes = 
clusterCoordinator.getNodeIdentifiers(NodeConnectionState.CONNECTING, 
NodeConnectionState.DISCONNECTED, NodeConnectionState.DISCONNECTING);
+            if (!disconnectedNodes.isEmpty()) {
+                throw new IllegalStateException("Cannot create an Asset 
because the following %s nodes are not currently connected: 
%s".formatted(disconnectedNodes.size(), disconnectedNodes));
+            }
+        }
+
+        // Get the context or throw ResourceNotFoundException
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final ParameterContextEntity contextEntity = 
serviceFacade.getParameterContext(contextId, false, user);
+        final Set<AffectedComponentEntity> affectedComponents = 
serviceFacade.getComponentsAffectedByParameterContextUpdate(Collections.singletonList(contextEntity.getComponent()));
+
+        // Authorize the request
+        serviceFacade.authorizeAccess(lookup -> {
+            // Verify READ and WRITE permissions for user, for the Parameter 
Context itself
+            final ParameterContext parameterContext = 
lookup.getParameterContext(contextId);
+            parameterContext.authorize(authorizer, RequestAction.READ, user);
+            parameterContext.authorize(authorizer, RequestAction.WRITE, user);
+
+            // Verify READ and WRITE permissions for user, for every component 
that is affected
+            affectedComponents.forEach(component -> 
parameterUpdateManager.authorizeAffectedComponent(component, lookup, user, 
true, true));
+        });
+
+        // If we need to replicate the request, we do so using the Upload 
Request Replicator, rather than the typical replicate() method.
+        // This is because Upload Request Replication works differently in 
that it needs to be able to replicate the InputStream multiple times,
+        // so it must create a file on disk to do so and then use the file's 
content to replicate the request. It also bypasses the two-phase
+        // commit process that is used for other requests because doing so 
would result in uploading the file twice to each node or providing a
+        // different request for each of the two phases.
+
+        final long startTime = System.currentTimeMillis();
+        final InputStream maxLengthInputStream = new 
MaxLengthInputStream(assetContents, (long) DataUnit.GB.toB(1));

Review Comment:
   For tracking purposes, it would be helpful to promote the maximum length 
value to a static value. Although this could be made configurable, it seems 
unlikely to change, and 1 GB seems like it should be large enough to handle 
anything of reasonable size.



##########
nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/ParamContextClient.java:
##########
@@ -40,4 +43,10 @@ public interface ParamContextClient {
 
     ParameterContextUpdateRequestEntity deleteParamContextUpdateRequest(String 
contextId, String updateRequestId) throws NiFiClientException, IOException;
 
+    AssetEntity createAsset(String contextId, String assetName, File file) 
throws NiFiClientException, IOException;
+
+    AssetsEntity getAssets(String contextId) throws NiFiClientException, 
IOException;
+
+    File getAssetContent(String contextId, String assetId, File 
outputDirectory) throws NiFiClientException, IOException;

Review Comment:
   What do you think about returning a `Path` instead of `File` for this method?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to