diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index 338e90c1c4..caf87d0a18 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -664,6 +664,18 @@ language governing permissions and limitations under the License. -->
1.18.0-SNAPSHOT
nar
+
+ org.apache.nifi
+ nifi-smb-client-api-nar
+ 1.18.0-SNAPSHOT
+ nar
+
+
+ org.apache.nifi
+ nifi-smb-smbj-client-nar
+ 1.18.0-SNAPSHOT
+ nar
+
org.apache.nifi
nifi-windows-event-log-nar
diff --git a/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java b/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
index 922907c8dc..a92cb34575 100644
--- a/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
+++ b/nifi-mock/src/main/java/org/apache/nifi/util/StandardProcessorTestRunner.java
@@ -182,7 +182,7 @@ public class StandardProcessorTestRunner implements TestRunner {
@Override
public void run(final int iterations, final boolean stopOnFinish, final boolean initialize) {
- run(iterations, stopOnFinish, initialize, 5000);
+ run(iterations, stopOnFinish, initialize, 5000 + iterations * runSchedule);
}
@Override
diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-listed-entity/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java b/nifi-nar-bundles/nifi-extension-utils/nifi-listed-entity/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java
index 682450211e..30a0ca2baf 100644
--- a/nifi-nar-bundles/nifi-extension-utils/nifi-listed-entity/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java
+++ b/nifi-nar-bundles/nifi-extension-utils/nifi-listed-entity/src/main/java/org/apache/nifi/processor/util/list/AbstractListProcessor.java
@@ -543,6 +543,10 @@ public abstract class AbstractListProcessor extends Ab
return System.currentTimeMillis();
}
+ protected long getCurrentNanoTime() {
+ return System.nanoTime();
+ }
+
public void listByNoTracking(final ProcessContext context, final ProcessSession session) {
final List entityList;
@@ -655,6 +659,7 @@ public abstract class AbstractListProcessor extends Ab
.computeIfAbsent(entity.getTimestamp(), __ -> new ArrayList<>())
.add(entity)
);
+
if (getLogger().isTraceEnabled()) {
getLogger().trace("orderedEntries: " +
orderedEntries.values().stream()
@@ -744,8 +749,8 @@ public abstract class AbstractListProcessor extends Ab
}
final List entityList;
- final long currentRunTimeNanos = System.nanoTime();
- final long currentRunTimeMillis = System.currentTimeMillis();
+ final long currentRunTimeNanos = getCurrentNanoTime();
+ final long currentRunTimeMillis = getCurrentTime();
try {
// track of when this last executed for consideration of the lag nanos
entityList = performListing(context, minTimestampToListMillis, ListingMode.EXECUTION);
@@ -1100,7 +1105,7 @@ public abstract class AbstractListProcessor extends Ab
}
protected ListedEntityTracker createListedEntityTracker() {
- return new ListedEntityTracker<>(getIdentifier(), getLogger(), getRecordSchema());
+ return new ListedEntityTracker<>(getIdentifier(), getLogger(), this::getCurrentTime, getRecordSchema());
}
private void listByTrackingEntities(ProcessContext context, ProcessSession session) throws ProcessException {
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api-nar/pom.xml b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api-nar/pom.xml
new file mode 100644
index 0000000000..925d9b93ee
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api-nar/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ org.apache.nifi
+ nifi-smb-bundle
+ 1.18.0-SNAPSHOT
+
+ 4.0.0
+
+ nifi-smb-client-api-nar
+ nar
+
+
+ true
+ true
+
+
+
+
+ org.apache.nifi
+ nifi-standard-services-api-nar
+ 1.18.0-SNAPSHOT
+ nar
+
+
+ org.apache.nifi
+ nifi-smb-client-api
+ 1.18.0-SNAPSHOT
+
+
+
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/pom.xml b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/pom.xml
new file mode 100644
index 0000000000..1c3a3b04b2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ org.apache.nifi
+ nifi-smb-bundle
+ 1.18.0-SNAPSHOT
+
+ nifi-smb-client-api
+ jar
+
+
+ org.apache.nifi
+ nifi-api
+ 1.18.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-listed-entity
+ 1.18.0-SNAPSHOT
+
+
+ org.apache.nifi
+ nifi-record
+
+
+
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientProviderService.java b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientProviderService.java
new file mode 100644
index 0000000000..3dc23f1aa0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientProviderService.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.services.smb;
+
+import java.io.IOException;
+import java.net.URI;
+import org.apache.nifi.controller.ControllerService;
+
+public interface SmbClientProviderService extends ControllerService {
+
+ /**
+ * Returns the identifier of the service location.
+ *
+ * @return the remote location
+ */
+ URI getServiceLocation();
+
+ /**
+ * Returns the smb client to use.
+ *
+ * @return the client.
+ */
+ SmbClientService getClient() throws IOException;
+
+}
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientService.java b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientService.java
new file mode 100644
index 0000000000..4ecaf9a507
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbClientService.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.services.smb;
+
+import java.util.stream.Stream;
+
+/**
+ * Service abstraction for Server Message Block protocol operations.
+ */
+public interface SmbClientService extends AutoCloseable {
+
+ Stream listRemoteFiles(String path);
+
+ void createDirectory(String path);
+
+}
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbListableEntity.java b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbListableEntity.java
new file mode 100644
index 0000000000..da33602540
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-client-api/src/main/java/org/apache/nifi/services/smb/SmbListableEntity.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.services.smb;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.nifi.processor.util.list.ListableEntity;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+
+public class SmbListableEntity implements ListableEntity {
+
+ private final String name;
+ private final String shortName;
+ private final String path;
+ private final long timestamp;
+ private final long creationTime;
+ private final long lastAccessTime;
+ private final long changeTime;
+ private final boolean directory;
+ private final long size;
+ private final long allocationSize;
+
+ private SmbListableEntity(String name, String shortName, String path, long timestamp, long creationTime,
+ long lastAccessTime, long changeTime, boolean directory,
+ long size, long allocationSize) {
+ this.name = name;
+ this.shortName = shortName;
+ this.path = path;
+ this.timestamp = timestamp;
+ this.creationTime = creationTime;
+ this.lastAccessTime = lastAccessTime;
+ this.changeTime = changeTime;
+ this.directory = directory;
+ this.size = size;
+ this.allocationSize = allocationSize;
+ }
+
+ public static SimpleRecordSchema getRecordSchema() {
+ List fields = Arrays.asList(
+ new RecordField("filename", RecordFieldType.STRING.getDataType(), false),
+ new RecordField("shortName", RecordFieldType.STRING.getDataType(), false),
+ new RecordField("path", RecordFieldType.STRING.getDataType(), false),
+ new RecordField("identifier", RecordFieldType.STRING.getDataType(), false),
+ new RecordField("timestamp", RecordFieldType.LONG.getDataType(), false),
+ new RecordField("creationTime", RecordFieldType.LONG.getDataType(), false),
+ new RecordField("lastAccessTime", RecordFieldType.LONG.getDataType(), false),
+ new RecordField("changeTime", RecordFieldType.LONG.getDataType(), false),
+ new RecordField("size", RecordFieldType.LONG.getDataType(), false),
+ new RecordField("allocationSize", RecordFieldType.LONG.getDataType(), false)
+ );
+ return new SimpleRecordSchema(fields);
+ }
+
+ public static SmbListableEntityBuilder builder() {
+ return new SmbListableEntityBuilder();
+ }
+
+ public String getShortName() {
+ return shortName;
+ }
+
+ public long getCreationTime() {
+ return creationTime;
+ }
+
+ public long getLastAccessTime() {
+ return lastAccessTime;
+ }
+
+ public long getChangeTime() {
+ return changeTime;
+ }
+
+ public long getAllocationSize() {
+ return allocationSize;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getPathWithName() {
+ return path.isEmpty() ? name : path + "/" + name;
+ }
+
+ @Override
+ public String getIdentifier() {
+ return getPathWithName();
+ }
+
+ @Override
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SmbListableEntity that = (SmbListableEntity) o;
+ return getPathWithName().equals(that.getPathWithName());
+ }
+
+ @Override
+ public int hashCode() {
+ return getPathWithName().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return getPathWithName() + " (last write: " + timestamp + " size: " + size + ")";
+ }
+
+ @Override
+ public Record toRecord() {
+ final Map record = new TreeMap<>();
+ record.put("filename", getName());
+ record.put("shortName", getShortName());
+ record.put("path", path);
+ record.put("identifier", getPathWithName());
+ record.put("timestamp", getTimestamp());
+ record.put("creationTime", getCreationTime());
+ record.put("lastAccessTime", getLastAccessTime());
+ record.put("size", getSize());
+ record.put("allocationSize", getAllocationSize());
+ return new MapRecord(getRecordSchema(), record);
+ }
+
+ public static class SmbListableEntityBuilder {
+
+ private String name;
+ private String shortName;
+ private String path = "";
+ private long timestamp;
+ private long creationTime;
+ private long lastAccessTime;
+ private long changeTime;
+ private boolean directory = false;
+ private long size = 0;
+ private long allocationSize = 0;
+
+ public SmbListableEntityBuilder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setShortName(String shortName) {
+ this.shortName = shortName;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setCreationTime(long creationTime) {
+ this.creationTime = creationTime;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setLastAccessTime(long lastAccessTime) {
+ this.lastAccessTime = lastAccessTime;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setChangeTime(long changeTime) {
+ this.changeTime = changeTime;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setDirectory(boolean directory) {
+ this.directory = directory;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setSize(long size) {
+ this.size = size;
+ return this;
+ }
+
+ public SmbListableEntityBuilder setAllocationSize(long allocationSize) {
+ this.allocationSize = allocationSize;
+ return this;
+ }
+
+ public SmbListableEntity build() {
+ return new SmbListableEntity(name, shortName, path, timestamp, creationTime, lastAccessTime, changeTime,
+ directory, size, allocationSize);
+ }
+ }
+
+}
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-nar/pom.xml b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-nar/pom.xml
index 25255fd5af..61c19970a9 100644
--- a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-nar/pom.xml
@@ -35,5 +35,11 @@
nifi-smb-processors
1.18.0-SNAPSHOT
+
+ org.apache.nifi
+ nifi-smb-client-api-nar
+ 1.18.0-SNAPSHOT
+ nar
+
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/pom.xml b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/pom.xml
index 58b04c5898..de266368b2 100644
--- a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/pom.xml
@@ -26,10 +26,24 @@
jar
+
+ org.apache.nifi
+ nifi-smb-client-api
+ 1.18.0-SNAPSHOT
+ provided
+
org.apache.nifi
nifi-api
+
+ org.apache.nifi
+ nifi-record
+
+
+ org.apache.nifi
+ nifi-record-serialization-service-api
+
org.apache.nifi
nifi-utils
@@ -38,7 +52,10 @@
com.hierynomus
smbj
- 0.10.0
+
+
+ commons-io
+ commons-io
org.apache.nifi
@@ -46,5 +63,26 @@
1.18.0-SNAPSHOT
test
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.apache.nifi
+ nifi-mock-record-utils
+ test
+
+
+ org.apache.nifi
+ nifi-smb-smbj-client
+ 1.18.0-SNAPSHOT
+ test
+
+
+ org.apache.nifi
+ nifi-distributed-cache-client-service-api
+ test
+
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/java/org/apache/nifi/processors/smb/ListSmb.java b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/java/org/apache/nifi/processors/smb/ListSmb.java
new file mode 100644
index 0000000000..e0029144f6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/java/org/apache/nifi/processors/smb/ListSmb.java
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.smb;
+
+import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Collections.unmodifiableMap;
+import static org.apache.nifi.components.state.Scope.CLUSTER;
+import static org.apache.nifi.processor.util.StandardValidators.DATA_SIZE_VALIDATOR;
+import static org.apache.nifi.processor.util.StandardValidators.NON_BLANK_VALIDATOR;
+import static org.apache.nifi.processor.util.StandardValidators.NON_EMPTY_VALIDATOR;
+import static org.apache.nifi.processor.util.StandardValidators.TIME_PERIOD_VALIDATOR;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
+import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
+import org.apache.nifi.annotation.behavior.Stateful;
+import org.apache.nifi.annotation.behavior.TriggerSerially;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyDescriptor.Builder;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.Validator;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.util.list.AbstractListProcessor;
+import org.apache.nifi.processor.util.list.ListedEntityTracker;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.services.smb.SmbClientProviderService;
+import org.apache.nifi.services.smb.SmbClientService;
+import org.apache.nifi.services.smb.SmbListableEntity;
+
+@PrimaryNodeOnly
+@TriggerSerially
+@Tags({"samba, smb, cifs, files", "list"})
+@SeeAlso({PutSmbFile.class, GetSmbFile.class})
+@CapabilityDescription("Lists concrete files shared via SMB protocol. " +
+ "Each listed file may result in one flowfile, the metadata being written as flowfile attributes. " +
+ "Or - in case the 'Record Writer' property is set - the entire result is written as records to a single flowfile. "
+ +
+ "This Processor is designed to run on Primary Node only in a cluster. If the primary node changes, the new Primary Node will pick up where the "
+ +
+ "previous node left off without duplicating all of the data.")
+@InputRequirement(Requirement.INPUT_FORBIDDEN)
+@WritesAttributes({
+ @WritesAttribute(attribute = "filename", description = "The name of the file that was read from filesystem."),
+ @WritesAttribute(attribute = "shortname", description = "The short name of the file that was read from filesystem."),
+ @WritesAttribute(attribute = "path", description =
+ "The path is set to the relative path of the file's directory "
+ + "on filesystem compared to the Share and Input Directory properties and the configured host "
+ + "and port inherited from the configured connection pool controller service. For example, for "
+ + "a given remote location smb://HOSTNAME:PORT/SHARE/DIRECTORY, and a file is being listed from "
+ + "smb://HOSTNAME:PORT/SHARE/DIRECTORY/sub/folder/file then the path attribute will be set to \"sub/folder/file\"."),
+ @WritesAttribute(attribute = "absolute.path", description =
+ "The absolute.path is set to the absolute path of the file's directory on the remote location. For example, "
+ + "given a remote location smb://HOSTNAME:PORT/SHARE/DIRECTORY, and a file is being listen from "
+ + "SHARE/DIRECTORY/sub/folder/file then the absolute.path attribute will be set to "
+ + "SHARE/DIRECTORY/sub/folder/file."),
+ @WritesAttribute(attribute = "identifier", description =
+ "The identifier of the file. This equals to the path attribute so two files with the same relative path "
+ + "coming from different file shares considered to be identical."),
+ @WritesAttribute(attribute = "timestamp", description =
+ "The timestamp of when the file's content changed in the filesystem as 'yyyy-MM-dd'T'HH:mm:ssZ'"),
+ @WritesAttribute(attribute = "createTime", description =
+ "The timestamp of when the file was created in the filesystem as 'yyyy-MM-dd'T'HH:mm:ssZ'"),
+ @WritesAttribute(attribute = "lastAccessTime", description =
+ "The timestamp of when the file was accessed in the filesystem as 'yyyy-MM-dd'T'HH:mm:ssZ'"),
+ @WritesAttribute(attribute = "changeTime", description =
+ "The timestamp of when the file's attributes was changed in the filesystem as 'yyyy-MM-dd'T'HH:mm:ssZ'"),
+ @WritesAttribute(attribute = "size", description = "The number of bytes in the source file"),
+ @WritesAttribute(attribute = "allocationSize", description = "The number of bytes allocated for the file on the server"),
+})
+@Stateful(scopes = {Scope.CLUSTER}, description =
+ "After performing a listing of files, the state of the previous listing can be stored in order to list files "
+ + "continuously without duplication."
+)
+public class ListSmb extends AbstractListProcessor {
+
+ public static final PropertyDescriptor DIRECTORY = new PropertyDescriptor.Builder()
+ .displayName("Input Directory")
+ .name("directory")
+ .description("The network folder from which to list files. This is the remaining relative path " +
+ "after the share: smb://HOSTNAME:PORT/SHARE/[DIRECTORY]/sub/directories. It is also possible "
+ + "to add subdirectories. The given path on the remote file share must exist. "
+ + "This can be checked using verification. You may mix Windows and Linux-style "
+ + "directory separators.")
+ .required(false)
+ .addValidator(NON_BLANK_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor MINIMUM_AGE = new PropertyDescriptor.Builder()
+ .displayName("Minimum File Age")
+ .name("min-file-age")
+ .description("The minimum age that a file must be in order to be listed; any file younger than this "
+ + "amount of time will be ignored.")
+ .required(true)
+ .addValidator(TIME_PERIOD_VALIDATOR)
+ .defaultValue("5 secs")
+ .build();
+
+ public static final PropertyDescriptor MAXIMUM_AGE = new PropertyDescriptor.Builder()
+ .displayName("Maximum File Age")
+ .name("max-file-age")
+ .description("Any file older than the given value will be omitted. ")
+ .required(false)
+ .addValidator(TIME_PERIOD_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor MINIMUM_SIZE = new PropertyDescriptor.Builder()
+ .displayName("Minimum File Size")
+ .name("min-file-size")
+ .description("Any file smaller than the given value will be omitted.")
+ .required(false)
+ .addValidator(DATA_SIZE_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor MAXIMUM_SIZE = new PropertyDescriptor.Builder()
+ .displayName("Maximum File Size")
+ .name("max-file-size")
+ .description("Any file larger than the given value will be omitted.")
+ .required(false)
+ .addValidator(DATA_SIZE_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor SMB_LISTING_STRATEGY = new PropertyDescriptor.Builder()
+ .fromPropertyDescriptor(LISTING_STRATEGY)
+ .allowableValues(BY_ENTITIES, NO_TRACKING, BY_TIMESTAMPS)
+ .build();
+
+ public static final PropertyDescriptor SMB_CLIENT_PROVIDER_SERVICE = new Builder()
+ .name("smb-client-provider-service")
+ .displayName("SMB Client Provider Service")
+ .description("Specifies the SMB client provider to use for creating SMB connections.")
+ .required(true)
+ .identifiesControllerService(SmbClientProviderService.class)
+ .build();
+
+ public static final PropertyDescriptor FILE_NAME_SUFFIX_FILTER = new Builder()
+ .name("file-name-suffix-filter")
+ .displayName("File Name Suffix Filter")
+ .description("Files ending with the given suffix will be omitted. Can be used to make sure that files "
+ + "that are still uploading are not listed multiple times, by having those files have a suffix "
+ + "and remove the suffix once the upload finishes. This is highly recommended when using "
+ + "'Tracking Entities' or 'Tracking Timestamps' listing strategies.")
+ .required(false)
+ .addValidator(NON_EMPTY_VALIDATOR)
+ .addValidator(new MustNotContainDirectorySeparatorsValidator())
+ .build();
+
+ private static final List PROPERTIES = unmodifiableList(asList(
+ SMB_LISTING_STRATEGY,
+ SMB_CLIENT_PROVIDER_SERVICE,
+ DIRECTORY,
+ AbstractListProcessor.RECORD_WRITER,
+ FILE_NAME_SUFFIX_FILTER,
+ MINIMUM_AGE,
+ MAXIMUM_AGE,
+ MINIMUM_SIZE,
+ MAXIMUM_SIZE,
+ AbstractListProcessor.TARGET_SYSTEM_TIMESTAMP_PRECISION,
+ ListedEntityTracker.TRACKING_STATE_CACHE,
+ ListedEntityTracker.TRACKING_TIME_WINDOW,
+ ListedEntityTracker.INITIAL_LISTING_TARGET
+ ));
+
+ @Override
+ protected List getSupportedPropertyDescriptors() {
+ return PROPERTIES;
+ }
+
+ @Override
+ protected Map createAttributes(SmbListableEntity entity, ProcessContext context) {
+ final Map attributes = new TreeMap<>();
+ attributes.put("filename", entity.getName());
+ attributes.put("shortname", entity.getShortName());
+ attributes.put("path", entity.getPath());
+ attributes.put("absolute.path", entity.getPathWithName());
+ attributes.put("identifier", entity.getIdentifier());
+ attributes.put("timestamp", formatTimeStamp(entity.getTimestamp()));
+ attributes.put("creationTime", formatTimeStamp(entity.getCreationTime()));
+ attributes.put("lastAccessTime", formatTimeStamp(entity.getLastAccessTime()));
+ attributes.put("changeTime", formatTimeStamp(entity.getChangeTime()));
+ attributes.put("size", String.valueOf(entity.getSize()));
+ attributes.put("allocationSize", String.valueOf(entity.getAllocationSize()));
+ return unmodifiableMap(attributes);
+ }
+
+ @Override
+ protected String getPath(ProcessContext context) {
+ final SmbClientProviderService clientProviderService =
+ context.getProperty(SMB_CLIENT_PROVIDER_SERVICE).asControllerService(SmbClientProviderService.class);
+ final URI serviceLocation = clientProviderService.getServiceLocation();
+ final String directory = getDirectory(context);
+ return String.format("%s/%s", serviceLocation.toString(), directory.isEmpty() ? "" : directory + "/");
+ }
+
+ @Override
+ protected List performListing(ProcessContext context, Long minimumTimestampOrNull,
+ ListingMode listingMode) throws IOException {
+
+ final Predicate fileFilter =
+ createFileFilter(context, minimumTimestampOrNull);
+
+ try (Stream listing = performListing(context)) {
+ final Iterator iterator = listing.iterator();
+ final List result = new LinkedList<>();
+ while (iterator.hasNext()) {
+ if (isExecutionStopped(listingMode)) {
+ return emptyList();
+ }
+ final SmbListableEntity entity = iterator.next();
+ if (fileFilter.test(entity)) {
+ result.add(entity);
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ throw new IOException("Could not perform listing", e);
+ }
+ }
+
+ @Override
+ protected boolean isListingResetNecessary(PropertyDescriptor property) {
+ return asList(SMB_CLIENT_PROVIDER_SERVICE, DIRECTORY, FILE_NAME_SUFFIX_FILTER).contains(property);
+ }
+
+ @Override
+ protected Scope getStateScope(PropertyContext context) {
+ return CLUSTER;
+ }
+
+ @Override
+ protected RecordSchema getRecordSchema() {
+ return SmbListableEntity.getRecordSchema();
+ }
+
+ @Override
+ protected Integer countUnfilteredListing(ProcessContext context) throws IOException {
+ try (Stream listing = performListing(context)) {
+ return Long.valueOf(listing.count()).intValue();
+ } catch (Exception e) {
+ throw new IOException("Could not count files", e);
+ }
+ }
+
+ @Override
+ protected String getListingContainerName(ProcessContext context) {
+ return String.format("Remote Directory [%s]", getPath(context));
+ }
+
+ private String formatTimeStamp(long timestamp) {
+ return ISO_DATE_TIME.format(
+ LocalDateTime.ofEpochSecond(TimeUnit.MILLISECONDS.toSeconds(timestamp), 0, ZoneOffset.UTC));
+ }
+
+ private boolean isExecutionStopped(ListingMode listingMode) {
+ return ListingMode.EXECUTION.equals(listingMode) && !isScheduled();
+ }
+
+ private Predicate createFileFilter(ProcessContext context, Long minTimestampOrNull) {
+
+ final Long minimumAge = context.getProperty(MINIMUM_AGE).asTimePeriod(TimeUnit.MILLISECONDS);
+ final Long maximumAgeOrNull = context.getProperty(MAXIMUM_AGE).isSet() ? context.getProperty(MAXIMUM_AGE)
+ .asTimePeriod(TimeUnit.MILLISECONDS) : null;
+ final Double minimumSizeOrNull =
+ context.getProperty(MINIMUM_SIZE).isSet() ? context.getProperty(MINIMUM_SIZE).asDataSize(DataUnit.B)
+ : null;
+ final Double maximumSizeOrNull =
+ context.getProperty(MAXIMUM_SIZE).isSet() ? context.getProperty(MAXIMUM_SIZE).asDataSize(DataUnit.B)
+ : null;
+ final String suffixOrNull = context.getProperty(FILE_NAME_SUFFIX_FILTER).getValue();
+
+ final long now = getCurrentTime();
+ Predicate filter = entity -> now - entity.getTimestamp() >= minimumAge;
+
+ if (maximumAgeOrNull != null) {
+ filter = filter.and(entity -> now - entity.getTimestamp() <= maximumAgeOrNull);
+ }
+
+ if (minTimestampOrNull != null) {
+ filter = filter.and(entity -> entity.getTimestamp() >= minTimestampOrNull);
+ }
+
+ if (minimumSizeOrNull != null) {
+ filter = filter.and(entity -> entity.getSize() >= minimumSizeOrNull);
+ }
+
+ if (maximumSizeOrNull != null) {
+ filter = filter.and(entity -> entity.getSize() <= maximumSizeOrNull);
+ }
+
+ if (suffixOrNull != null) {
+ filter = filter.and(entity -> !entity.getName().endsWith(suffixOrNull));
+ }
+
+ return filter;
+ }
+
+ private Stream performListing(ProcessContext context) throws IOException {
+ final SmbClientProviderService clientProviderService =
+ context.getProperty(SMB_CLIENT_PROVIDER_SERVICE).asControllerService(SmbClientProviderService.class);
+ final String directory = getDirectory(context);
+ final SmbClientService clientService = clientProviderService.getClient();
+ return clientService.listRemoteFiles(directory).onClose(() -> {
+ try {
+ clientService.close();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not close samba client", e);
+ }
+ });
+ }
+
+ private String getDirectory(ProcessContext context) {
+ final PropertyValue property = context.getProperty(DIRECTORY);
+ final String directory = property.isSet() ? property.getValue().replace('\\', '/') : "";
+ return directory.equals("/") ? "" : directory;
+ }
+
+ private static class MustNotContainDirectorySeparatorsValidator implements Validator {
+
+ @Override
+ public ValidationResult validate(String subject, String value, ValidationContext context) {
+ return new ValidationResult.Builder()
+ .subject(subject)
+ .input(value)
+ .valid(!value.contains("/"))
+ .explanation(subject + " must not contain any folder separator character.")
+ .build();
+ }
+
+ }
+
+}
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index bc5320b99b..0dd9276bc0 100644
--- a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -12,5 +12,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+org.apache.nifi.processors.smb.GetSmbFile
+org.apache.nifi.processors.smb.ListSmb
org.apache.nifi.processors.smb.PutSmbFile
-org.apache.nifi.processors.smb.GetSmbFile
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/test/java/org/apache/nifi/processors/smb/ListSmbIT.java b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/test/java/org/apache/nifi/processors/smb/ListSmbIT.java
new file mode 100644
index 0000000000..8ef5aa7ba3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-smb-bundle/nifi-smb-processors/src/test/java/org/apache/nifi/processors/smb/ListSmbIT.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.smb;
+
+import static java.util.Arrays.asList;
+import static java.util.Arrays.fill;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.nifi.processor.util.list.AbstractListProcessor.LISTING_STRATEGY;
+import static org.apache.nifi.processor.util.list.AbstractListProcessor.RECORD_WRITER;
+import static org.apache.nifi.processor.util.list.AbstractListProcessor.REL_SUCCESS;
+import static org.apache.nifi.processors.smb.ListSmb.DIRECTORY;
+import static org.apache.nifi.processors.smb.ListSmb.FILE_NAME_SUFFIX_FILTER;
+import static org.apache.nifi.processors.smb.ListSmb.MINIMUM_AGE;
+import static org.apache.nifi.processors.smb.ListSmb.MINIMUM_SIZE;
+import static org.apache.nifi.processors.smb.ListSmb.SMB_CLIENT_PROVIDER_SERVICE;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.DOMAIN;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.HOSTNAME;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.PASSWORD;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.PORT;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.SHARE;
+import static org.apache.nifi.services.smb.SmbjClientProviderService.USERNAME;
+import static org.apache.nifi.util.TestRunners.newTestRunner;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.MockRecordWriter;
+import org.apache.nifi.services.smb.SmbClientProviderService;
+import org.apache.nifi.services.smb.SmbListableEntity;
+import org.apache.nifi.services.smb.SmbjClientProviderService;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.DockerImageName;
+
+public class ListSmbIT {
+
+ private final static Integer DEFAULT_SAMBA_PORT = 445;
+ private final static Logger logger = LoggerFactory.getLogger(ListSmbTest.class);
+ private final GenericContainer> sambaContainer = new GenericContainer<>(DockerImageName.parse("dperson/samba"))
+ .withExposedPorts(DEFAULT_SAMBA_PORT, 139)
+ .waitingFor(Wait.forListeningPort())
+ .withLogConsumer(new Slf4jLogConsumer(logger))
+ .withCommand("-w domain -u username;password -s share;/folder;;no;no;username;;; -p");
+
+ @BeforeEach
+ public void beforeEach() {
+ sambaContainer.start();
+ }
+
+ @AfterEach
+ public void afterEach() {
+ sambaContainer.stop();
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {4, 50, 45000})
+ public void shouldFillSizeAttributeProperly(int size) throws Exception {
+ writeFile("1.txt", generateContentWithSize(size));
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ testRunner.setProperty(LISTING_STRATEGY, "none");
+ testRunner.setProperty(MINIMUM_AGE, "0 ms");
+ SmbjClientProviderService smbjClientProviderService = configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.enableControllerService(smbjClientProviderService);
+ testRunner.run();
+ testRunner.assertTransferCount(REL_SUCCESS, 1);
+ testRunner.getFlowFilesForRelationship(REL_SUCCESS)
+ .forEach(flowFile -> assertEquals(size, Integer.valueOf(flowFile.getAttribute("size"))));
+ testRunner.assertValid();
+ testRunner.disableControllerService(smbjClientProviderService);
+ }
+
+ @Test
+ public void shouldShowBulletinOnMissingDirectory() throws Exception {
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ testRunner.setProperty(LISTING_STRATEGY, "none");
+ testRunner.setProperty(MINIMUM_AGE, "0 ms");
+ testRunner.setProperty(DIRECTORY, "folderDoesNotExists");
+ SmbjClientProviderService smbjClientProviderService = configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.enableControllerService(smbjClientProviderService);
+ testRunner.run();
+ assertEquals(1, testRunner.getLogger().getErrorMessages().size());
+ testRunner.assertValid();
+ testRunner.disableControllerService(smbjClientProviderService);
+ }
+
+ @Test
+ public void shouldShowBulletinWhenShareIsInvalid() throws Exception {
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ SmbjClientProviderService smbjClientProviderService = configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.setProperty(smbjClientProviderService, SHARE, "invalid_share");
+ testRunner.enableControllerService(smbjClientProviderService);
+ testRunner.run();
+ assertEquals(1, testRunner.getLogger().getErrorMessages().size());
+ testRunner.assertValid();
+ testRunner.disableControllerService(smbjClientProviderService);
+ }
+
+ @Test
+ public void shouldShowBulletinWhenSMBPortIsInvalid() throws Exception {
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ final SmbClientProviderService smbClientProviderService =
+ configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.setProperty(smbClientProviderService, PORT, "1");
+ testRunner.enableControllerService(smbClientProviderService);
+ testRunner.run();
+ assertEquals(1, testRunner.getLogger().getErrorMessages().size());
+ testRunner.assertValid();
+ testRunner.disableControllerService(smbClientProviderService);
+ }
+
+ @Test
+ public void shouldShowBulletinWhenSMBHostIsInvalid() throws Exception {
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ final SmbClientProviderService smbClientProviderService =
+ configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.setProperty(smbClientProviderService, HOSTNAME, "this.host.should.not.exists");
+ testRunner.enableControllerService(smbClientProviderService);
+ testRunner.run();
+ assertEquals(1, testRunner.getLogger().getErrorMessages().size());
+ testRunner.disableControllerService(smbClientProviderService);
+ }
+
+ @Test
+ public void shouldUseRecordWriterProperly() throws Exception {
+ final Set testFiles = new HashSet<>(asList(
+ "1.txt",
+ "directory/2.txt",
+ "directory/subdirectory/3.txt",
+ "directory/subdirectory2/4.txt",
+ "directory/subdirectory3/5.txt"
+ ));
+ testFiles.forEach(file -> writeFile(file, generateContentWithSize(4)));
+
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ final MockRecordWriter writer = new MockRecordWriter(null, false);
+ final SimpleRecordSchema simpleRecordSchema = SmbListableEntity.getRecordSchema();
+ testRunner.addControllerService("writer", writer);
+ testRunner.enableControllerService(writer);
+ testRunner.setProperty(LISTING_STRATEGY, "none");
+ testRunner.setProperty(RECORD_WRITER, "writer");
+ testRunner.setProperty(MINIMUM_AGE, "0 ms");
+ SmbjClientProviderService smbjClientProviderService = configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.enableControllerService(smbjClientProviderService);
+ testRunner.run();
+ testRunner.assertTransferCount(REL_SUCCESS, 1);
+ final String result = testRunner.getFlowFilesForRelationship(REL_SUCCESS).get(0).getContent();
+ final int identifierColumnIndex = simpleRecordSchema.getFieldNames().indexOf("identifier");
+ final Set actual = Arrays.stream(result.split("\n"))
+ .map(row -> row.split(",")[identifierColumnIndex])
+ .collect(toSet());
+ assertEquals(testFiles, actual);
+ testRunner.assertValid();
+ testRunner.disableControllerService(smbjClientProviderService);
+ }
+
+ @Test
+ public void shouldWriteFlowFileAttributesProperly() throws Exception {
+ final Set testFiles = new HashSet<>(asList(
+ "file_name", "directory/file_name", "directory/subdirectory/file_name"
+ ));
+ testFiles.forEach(file -> writeFile(file, generateContentWithSize(4)));
+ final TestRunner testRunner = newTestRunner(ListSmb.class);
+ final SmbjClientProviderService smbjClientProviderService =
+ configureTestRunnerForSambaDockerContainer(testRunner);
+ testRunner.setProperty(LISTING_STRATEGY, "none");
+ testRunner.setProperty(MINIMUM_AGE, "0 sec");
+ testRunner.enableControllerService(smbjClientProviderService);
+ testRunner.run(1);
+ testRunner.assertTransferCount(REL_SUCCESS, 3);
+ final Set