[Folderwatcher] AWS S3 buckets monitoring support (#14669)

* Add S3 Thing

Signed-off-by: Alexandr Salamatov <goopilot@gmail.com>
This commit is contained in:
goopilot 2023-04-15 14:22:01 -05:00 committed by GitHub
parent 2c006ccd31
commit 3b2c574684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 800 additions and 38 deletions

View File

@ -1,54 +1,66 @@
# FolderWatcher Binding
This binding is intended to monitor FTP and local folder and its subfolders and notify of new files
This binding is intended to monitor FTP, local folder and S3 bucket and its subfolders and notify of new files
## Supported Things
Currently the binding support two types of things: `ftpfolder` and `localfolder`.
Currently the binding support three types of things: `ftpfolder`, `localfolder` and `s3bucket`.
## Thing Configuration
The `ftpfolder` thing has the following configuration options:
| Parameter | Name | Description | Required | Default value |
|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------|
| ftpAddress | FTP server | IP address of FTP server | yes | n/a |
| ftpPort | FTP port | Port of FTP server | yes | 21 |
| secureMode | FTP Security | FTP Security | yes | None |
| ftpUsername | Username | FTP user name | yes | n/a |
| ftpPassword | Password | FTP password | yes | n/a |
| ftpDir | RootDir | Root directory to be watched | yes | n/a |
| listRecursiveFtp | List Sub Folders | Allow listing of sub folders | yes | No |
| listHidden | List Hidden | Allow listing of hidden files | yes | false |
| connectionTimeout | Connection timeout, s | Connection timeout for FTP request | yes | 30 |
| pollInterval | Polling interval, s | Interval for polling folder changes | yes | 60 |
| diffHours | Time stamp difference, h | How many hours back to analyze | yes | 24 |
| Parameter | Name | Description | Required | Default value |
|-------------------|--------------------------|-------------------------------------|----------|---------------|
| ftpAddress | FTP server | IP address of FTP server | yes | n/a |
| ftpPort | FTP port | Port of FTP server | yes | 21 |
| secureMode | FTP Security | FTP Security | yes | None |
| ftpUsername | Username | FTP user name | yes | n/a |
| ftpPassword | Password | FTP password | yes | n/a |
| ftpDir | RootDir | Root directory to be watched | yes | n/a |
| listRecursiveFtp | List Sub Folders | Allow listing of sub folders | yes | No |
| listHidden | List Hidden | Allow listing of hidden files | yes | false |
| connectionTimeout | Connection timeout, s | Connection timeout for FTP request | yes | 30 |
| pollInterval | Polling interval, s | Interval for polling folder changes | yes | 60 |
| diffHours | Time stamp difference, h | How many hours back to analyze | yes | 24 |
The `localfolder` thing has the following configuration options:
| Parameter | Name | Description | Required | Default value |
|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------|
| localDir | Local Directory | Local directory to be watched | yes | n/a |
| listHiddenLocal | List Hidden | Allow listing of hidden files | yes | No |
| pollIntervalLocal | Polling interval, s | Interval for polling folder changes | yes | 60 |
| listRecursiveLocal | List Sub Folders | Allow listing of sub folders | yes | No |
| Parameter | Name | Description | Required | Default value |
|--------------------|---------------------|-------------------------------------|----------|---------------|
| localDir | Local Directory | Local directory to be watched | yes | n/a |
| listHiddenLocal | List Hidden | Allow listing of hidden files | yes | No |
| pollIntervalLocal | Polling interval, s | Interval for polling folder changes | yes | 60 |
| listRecursiveLocal | List Sub Folders | Allow listing of sub folders | yes | No |
The `s3bucket` thing has the following configuration options:
| Parameter | Name | Description | Required | Default value |
|----------------|----------------------|----------------------------------------------------|----------|---------------|
| s3BucketName | S3 Bucket Name | Name of the S3 bucket to be watched | yes | n/a |
| s3Path | S3 Path | S3 path (folder) to be monitored | no | n/a |
| pollIntervalS3 | Polling Interval | Interval for polling S3 bucket changes, in seconds | yes | 60 |
| awsKey | AWS Access Key | AWS access key | no | n/a |
| awsSecret | AWS Secret | AWS secret | no | n/a |
| awsRegion | AWS Region | AWS region of S3 bucket | yes | "" |
| s3Anonymous | Anonymous Connection | Connect anonymously (works for public buckets) | yes | true |
## Events
This binding currently supports the following events:
| Channel Type ID | Item Type | Description |
|-----------------|--------------|----------------------------------------------------------------------------------------|
| newftpfile | String | A new file name discovered on FTP |
| newlocalfile | String | A new file name discovered on in local folder |
| Channel Type ID | Item Type | Description |
|-----------------|-----------|----------------------------|
| newfile | String | A new file name discovered |
## Full Example
Thing configuration:
```java
folderwatcher:localfolder:myLocalFolder [ localDir="/myfolder", pollIntervalLocal=60, listHiddenLocal="false", listRecursiveLocal="false" ]
folderwatcher:ftpfolder:myLocalFolder [ ftpAddress="X.X.X.X", ftpPort=21, secureMode="EXPLICIT", ftpUsername="username", ftpPassword="password",ftpDir="/myfolder/",listHidden="true",listRecursiveFtp="true",connectionTimeout=33,pollInterval=66,diffHours=25]
folderwatcher:localfolder:myLocalFolder [ localDir="/myfolder", pollIntervalLocal=60, listHiddenLocal="false", listRecursiveLocal="false" ]
folderwatcher:ftpfolder:myLocalFolder [ ftpAddress="X.X.X.X", ftpPort=21, secureMode="EXPLICIT", ftpUsername="username", ftpPassword="password", ftpDir="/myfolder/", listHidden="true", listRecursiveFtp="true", connectionTimeout=33, pollInterval=66, diffHours=25 ]
folderwatcher:s3bucket:myS3bucket [ s3BucketName="mypublic-bucket", pollIntervalS3=60, awsRegion="us-west-1", s3Anonymous="true" ]
```
### Using in a rule:
@ -58,10 +70,10 @@ FTP example:
```java
rule "New FTP file"
when
Channel 'folderwatcher:ftpfolder:XXXXX:newfile' triggered
Channel "folderwatcher:ftpfolder:myLocalFolder:newfile" triggered
then
logInfo('NewFTPFile', receivedEvent.toString())
logInfo("NewFTPFile", receivedEvent.toString())
end
```
@ -71,10 +83,23 @@ Local folder example:
```java
rule "New Local file"
when
Channel 'folderwatcher:localfolder:XXXXX:newfile' triggered
Channel "folderwatcher:localfolder:myFTPFolder:newfile" triggered
then
logInfo('NewLocalFile', receivedEvent.toString())
logInfo("NewLocalFile", receivedEvent.toString())
end
```
S3 bucket example:
```java
rule "New S3 file"
when
Channel "folderwatcher:s3bucket:myS3bucket:newfile" triggered
then
logInfo("NewS3File", receivedEvent.toString())
end
```

View File

@ -26,5 +26,6 @@ public class FolderWatcherBindingConstants {
private static final String BINDING_ID = "folderwatcher";
public static final ThingTypeUID THING_TYPE_FTPFOLDER = new ThingTypeUID(BINDING_ID, "ftpfolder");
public static final ThingTypeUID THING_TYPE_LOCALFOLDER = new ThingTypeUID(BINDING_ID, "localfolder");
public static final ThingTypeUID THING_TYPE_S3BUCKET = new ThingTypeUID(BINDING_ID, "s3bucket");
public static final String CHANNEL_NEWFILE = "newfile";
}

View File

@ -20,12 +20,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.folderwatcher.internal.handler.FtpFolderWatcherHandler;
import org.openhab.binding.folderwatcher.internal.handler.LocalFolderWatcherHandler;
import org.openhab.binding.folderwatcher.internal.handler.S3BucketWatcherHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link FolderWatcherHandlerFactory} is responsible for creating things and thing
@ -38,7 +42,13 @@ import org.osgi.service.component.annotations.Component;
public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_FTPFOLDER,
THING_TYPE_LOCALFOLDER);
THING_TYPE_LOCALFOLDER, THING_TYPE_S3BUCKET);
private HttpClientFactory httpClientFactory;
@Activate
public FolderWatcherHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@ -53,6 +63,8 @@ public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
return new FtpFolderWatcherHandler(thing);
} else if (THING_TYPE_LOCALFOLDER.equals(thingTypeUID)) {
return new LocalFolderWatcherHandler(thing);
} else if (THING_TYPE_S3BUCKET.equals(thingTypeUID)) {
return new S3BucketWatcherHandler(thing, httpClientFactory);
}
return null;
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.api;
import static org.eclipse.jetty.http.HttpHeader.*;
import static org.eclipse.jetty.http.HttpMethod.*;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.folderwatcher.internal.api.auth.AWS4SignerBase;
import org.openhab.binding.folderwatcher.internal.api.auth.AWS4SignerForAuthorizationHeader;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* The {@link S3Actions} class contains AWS S3 API implementation.
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class S3Actions {
private final HttpClient httpClient;
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
private static final String CONTENT_TYPE = "application/xml";
private URL bucketUri;
private String region;
private String awsAccessKey;
private String awsSecretKey;
public S3Actions(HttpClientFactory httpClientFactory, String bucketName, String region) {
this(httpClientFactory, bucketName, region, "", "");
}
public S3Actions(HttpClientFactory httpClientFactory, String bucketName, String region, String awsAccessKey,
String awsSecretKey) {
this.httpClient = httpClientFactory.getCommonHttpClient();
try {
this.bucketUri = new URL("http://" + bucketName + ".s3." + region + ".amazonaws.com");
} catch (MalformedURLException e) {
throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
}
this.region = region;
this.awsAccessKey = awsAccessKey;
this.awsSecretKey = awsSecretKey;
}
public List<String> listBucket(String prefix) throws Exception {
Map<String, String> headers = new HashMap<String, String>();
Map<String, String> params = new HashMap<String, String>();
return listObjectsV2(prefix, headers, params);
}
private List<String> listObjectsV2(String prefix, Map<String, String> headers, Map<String, String> params)
throws Exception {
params.put("list-type", "2");
params.put("prefix", prefix);
if (!awsAccessKey.isEmpty() || !awsSecretKey.isEmpty()) {
headers.put("x-amz-content-sha256", AWS4SignerBase.EMPTY_BODY_SHA256);
AWS4SignerForAuthorizationHeader signer = new AWS4SignerForAuthorizationHeader(this.bucketUri, "GET", "s3",
region);
String authorization = signer.computeSignature(headers, params, AWS4SignerBase.EMPTY_BODY_SHA256,
awsAccessKey, awsSecretKey);
headers.put("Authorization", authorization);
}
headers.put(ACCEPT.toString(), CONTENT_TYPE);
Request request = httpClient.newRequest(this.bucketUri.toString()) //
.method(GET) //
.timeout(REQUEST_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS); //
for (String headerKey : headers.keySet()) {
request.header(headerKey, headers.get(headerKey));
}
for (String paramKey : params.keySet()) {
request.param(paramKey, params.get(paramKey));
}
ContentResponse contentResponse = request.send();
if (contentResponse.getStatus() != 200) {
throw new Exception("HTTP Response is not 200");
}
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
InputSource is = new InputSource(new StringReader(contentResponse.getContentAsString()));
Document doc = docBuilder.parse(is);
NodeList nameNodesList = doc.getElementsByTagName("Key");
List<String> returnList = new ArrayList<>();
if (nameNodesList.getLength() == 0) {
throw new Exception("No files deceted in the bucket");
}
for (int i = 0; i < nameNodesList.getLength(); i++) {
returnList.add(nameNodesList.item(i).getFirstChild().getTextContent());
}
nameNodesList = doc.getElementsByTagName("IsTruncated");
if (nameNodesList.getLength() > 0) {
if (nameNodesList.item(0).getFirstChild().getTextContent().equals("true")) {
nameNodesList = doc.getElementsByTagName("NextContinuationToken");
if (nameNodesList.getLength() > 0) {
String continueToken = nameNodesList.item(0).getFirstChild().getTextContent();
params.clear();
headers.clear();
params.put("continuation-token", continueToken);
returnList.addAll(listObjectsV2(prefix, headers, params));
}
}
}
return returnList;
}
}

View File

@ -0,0 +1,192 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.api.auth;
import java.net.URL;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.folderwatcher.internal.api.util.BinaryUtils;
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;
/**
* The {@link AWS4SignerBase} class contains based methods for AWS S3 API authentication.
* <p>
* Based on offical AWS example {@see https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-examples-using-sdks.html}
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public abstract class AWS4SignerBase {
public static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
public static final String SCHEME = "AWS4";
public static final String ALGORITHM = "HMAC-SHA256";
public static final String TERMINATOR = "aws4_request";
public static final String ISO8601BasicFormat = "yyyyMMdd'T'HHmmss'Z'";
public static final String DateStringFormat = "yyyyMMdd";
protected URL endpointUrl;
protected String httpMethod;
protected String serviceName;
protected String regionName;
protected final SimpleDateFormat dateTimeFormat;
protected final SimpleDateFormat dateStampFormat;
public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
this.endpointUrl = endpointUrl;
this.httpMethod = httpMethod;
this.serviceName = serviceName;
this.regionName = regionName;
dateTimeFormat = new SimpleDateFormat(ISO8601BasicFormat);
dateTimeFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
dateStampFormat = new SimpleDateFormat(DateStringFormat);
dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
}
protected static String getCanonicalizeHeaderNames(Map<String, String> headers) {
List<String> sortedHeaders = new ArrayList<String>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
StringBuilder buffer = new StringBuilder();
for (String header : sortedHeaders) {
if (buffer.length() > 0) {
buffer.append(";");
}
buffer.append(header.toLowerCase());
}
return buffer.toString();
}
protected static String getCanonicalizedHeaderString(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
List<String> sortedHeaders = new ArrayList<String>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
StringBuilder buffer = new StringBuilder();
for (String key : sortedHeaders) {
buffer.append(key.toLowerCase().replaceAll("\\s+", " ") + ":" + headers.get(key).replaceAll("\\s+", " "));
buffer.append("\n");
}
return buffer.toString();
}
protected static String getCanonicalRequest(URL endpoint, String httpMethod, String queryParameters,
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) {
String canonicalRequest = httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters
+ "\n" + canonicalizedHeaders + "\n" + canonicalizedHeaderNames + "\n" + bodyHash;
return canonicalRequest;
}
protected static String getCanonicalizedResourcePath(URL endpoint) {
if (endpoint == null) {
return "/";
}
String path = endpoint.getPath();
if (path == null || path.isEmpty()) {
return "/";
}
String encodedPath = HttpUtils.urlEncode(path, true);
if (encodedPath.startsWith("/")) {
return encodedPath;
} else {
return "/".concat(encodedPath);
}
}
public static String getCanonicalizedQueryString(Map<String, String> parameters) {
if (parameters == null || parameters.isEmpty()) {
return "";
}
SortedMap<String, String> sorted = new TreeMap<String, String>();
Iterator<Map.Entry<String, String>> pairs = parameters.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
String key = pair.getKey();
String value = pair.getValue();
sorted.put(HttpUtils.urlEncode(key, false), HttpUtils.urlEncode(value, false));
}
StringBuilder builder = new StringBuilder();
pairs = sorted.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
builder.append(pair.getKey());
builder.append("=");
builder.append(pair.getValue());
if (pairs.hasNext()) {
builder.append("&");
}
}
return builder.toString();
}
protected static String getStringToSign(String scheme, String algorithm, String dateTime, String scope,
String canonicalRequest) {
String stringToSign = scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
+ BinaryUtils.toHex(hash(canonicalRequest));
return stringToSign;
}
public static byte[] hash(String text) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(text.getBytes("UTF-8"));
return md.digest();
} catch (Exception e) {
throw new RuntimeException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
public static byte[] hash(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
} catch (Exception e) {
throw new RuntimeException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
protected static byte[] sign(String stringData, byte[] key, String algorithm) {
try {
byte[] data = stringData.getBytes("UTF-8");
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.api.auth;
import java.net.URL;
import java.util.Date;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.folderwatcher.internal.api.util.BinaryUtils;
/**
* The {@link AWS4SignerForAuthorizationHeader} class contains methods for AWS S3 API authentication using HTTP(S)
* headers.
* <p>
* Based on offical AWS example {@see https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-examples-using-sdks.html}
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class AWS4SignerForAuthorizationHeader extends AWS4SignerBase {
public AWS4SignerForAuthorizationHeader(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
super(endpointUrl, httpMethod, serviceName, regionName);
}
public String computeSignature(Map<String, String> headers, Map<String, String> queryParameters, String bodyHash,
String awsAccessKey, String awsSecretKey) {
Date now = new Date();
String dateTimeStamp = dateTimeFormat.format(now);
headers.put("x-amz-date", dateTimeStamp);
String hostHeader = endpointUrl.getHost();
int port = endpointUrl.getPort();
if (port > -1) {
hostHeader.concat(":" + Integer.toString(port));
}
headers.put("Host", hostHeader);
String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod, canonicalizedQueryParameters,
canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
String dateStamp = dateStampFormat.format(now);
String scope = dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR;
String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope, canonicalRequest);
byte[] kSecret = (SCHEME + awsSecretKey).getBytes();
byte[] kDate = sign(dateStamp, kSecret, "HmacSHA256");
byte[] kRegion = sign(regionName, kDate, "HmacSHA256");
byte[] kService = sign(serviceName, kRegion, "HmacSHA256");
byte[] kSigning = sign(TERMINATOR, kService, "HmacSHA256");
byte[] signature = sign(stringToSign, kSigning, "HmacSHA256");
String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
String signedHeadersAuthorizationHeader = "SignedHeaders=" + canonicalizedHeaderNames;
String signatureAuthorizationHeader = "Signature=" + BinaryUtils.toHex(signature);
String authorizationHeader = SCHEME + "-" + ALGORITHM + " " + credentialsAuthorizationHeader + ", "
+ signedHeadersAuthorizationHeader + ", " + signatureAuthorizationHeader;
return authorizationHeader;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.api.util;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BinaryUtils} class contains methods for binary interactions.
* <p>
* Based on offical AWS example {@see https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-examples-using-sdks.html}
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class BinaryUtils {
public static String toHex(byte[] data) {
StringBuilder sb = new StringBuilder(data.length * 2);
for (int i = 0; i < data.length; i++) {
String hex = Integer.toHexString(data[i]);
if (hex.length() == 1) {
sb.append("0");
} else if (hex.length() == 8) {
hex = hex.substring(6);
}
sb.append(hex);
}
return sb.toString().toLowerCase(Locale.getDefault());
}
public static byte[] fromHex(String hexData) {
byte[] result = new byte[(hexData.length() + 1) / 2];
String hexNumber = null;
int stringOffset = 0;
int byteOffset = 0;
while (stringOffset < hexData.length()) {
hexNumber = hexData.substring(stringOffset, stringOffset + 2);
stringOffset += 2;
result[byteOffset++] = (byte) Integer.parseInt(hexNumber, 16);
}
return result;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.api.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HttpUtils} class contains metohdos related to HTTP(S).
* <p>
* Based on offical AWS example {@see https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-examples-using-sdks.html}
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class HttpUtils {
public static String urlEncode(String url, boolean keepPathSlash) {
String encoded;
try {
encoded = URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding is not supported.", e);
}
if (keepPathSlash) {
encoded = encoded.replace("%2F", "/");
}
return encoded;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LocalFolderWatcherConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class S3BucketWatcherConfiguration {
public String s3BucketName = "";
public String s3Path = "";
public boolean s3Anonymous;
public int pollIntervalS3;
public String awsKey = "";
public String awsSecret = "";
public String awsRegion = "";
}

View File

@ -108,9 +108,11 @@ public class FtpFolderWatcherHandler extends BaseThingHandler {
ScheduledFuture<?> initJob = this.initJob;
if (executionJob != null) {
executionJob.cancel(true);
this.executionJob = null;
}
if (initJob != null) {
initJob.cancel(true);
this.initJob = null;
}
if (ftp.isConnected()) {
try {

View File

@ -102,6 +102,7 @@ public class LocalFolderWatcherHandler extends BaseThingHandler {
ScheduledFuture<?> executionJob = this.executionJob;
if (executionJob != null) {
executionJob.cancel(true);
this.executionJob = null;
}
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.folderwatcher.internal.handler;
import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.folderwatcher.internal.api.S3Actions;
import org.openhab.binding.folderwatcher.internal.common.WatcherCommon;
import org.openhab.binding.folderwatcher.internal.config.S3BucketWatcherConfiguration;
import org.openhab.core.OpenHAB;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link S3BucketWatcherHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Alexandr Salamatov - Initial contribution
*/
@NonNullByDefault
public class S3BucketWatcherHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(S3BucketWatcherHandler.class);
private S3BucketWatcherConfiguration config = new S3BucketWatcherConfiguration();
private File currentS3ListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher"
+ File.separator + thing.getUID().getAsString().replace(':', '_') + ".data");
private @Nullable ScheduledFuture<?> executionJob;
private List<String> previousS3Listing = new ArrayList<>();
private HttpClientFactory httpClientFactory;
private @Nullable S3Actions s3;
public S3BucketWatcherHandler(Thing thing, HttpClientFactory httpClientFactory) {
super(thing);
this.httpClientFactory = httpClientFactory;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
if (command instanceof RefreshType) {
refreshS3BucketInformation();
}
}
@Override
public void initialize() {
config = getConfigAs(S3BucketWatcherConfiguration.class);
if (config.s3Anonymous) {
s3 = new S3Actions(httpClientFactory, config.s3BucketName, config.awsRegion);
} else {
s3 = new S3Actions(httpClientFactory, config.s3BucketName, config.awsRegion, config.awsKey,
config.awsSecret);
}
try {
previousS3Listing = WatcherCommon.initStorage(currentS3ListingFile, config.s3BucketName);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
logger.debug("Can't write file {}: {}", currentS3ListingFile, e.getMessage());
return;
}
if (refreshS3BucketInformation()) {
if (config.pollIntervalS3 > 0) {
updateStatus(ThingStatus.ONLINE);
executionJob = scheduler.scheduleWithFixedDelay(this::refreshS3BucketInformation, config.pollIntervalS3,
config.pollIntervalS3, TimeUnit.SECONDS);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Polling interval must be greater then 0 seconds");
return;
}
}
}
private boolean refreshS3BucketInformation() {
List<String> currentS3Listing = new ArrayList<>();
try {
currentS3Listing = s3.listBucket(config.s3Path);
List<String> difS3Listing = new ArrayList<>(currentS3Listing);
difS3Listing.removeAll(previousS3Listing);
difS3Listing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file));
if (!difS3Listing.isEmpty()) {
WatcherCommon.saveNewListing(difS3Listing, currentS3ListingFile);
}
previousS3Listing = new ArrayList<>(currentS3Listing);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Can't connect to the bucket");
logger.debug("Can't connect to the bucket: {}", e.getMessage());
return false;
}
return true;
}
@Override
public void dispose() {
ScheduledFuture<?> executionJob = this.executionJob;
if (executionJob != null) {
executionJob.cancel(true);
this.executionJob = null;
}
}
}

View File

@ -9,11 +9,13 @@ thing-type.folderwatcher.ftpfolder.label = FTP Folder
thing-type.folderwatcher.ftpfolder.description = FTP folder to be watched
thing-type.folderwatcher.localfolder.label = Local Folder
thing-type.folderwatcher.localfolder.description = Local folder to be watched
thing-type.folderwatcher.s3bucket.label = AWS S3 Bucket
thing-type.folderwatcher.s3bucket.description = AWS S3 bucket to be watched
# thing types config
thing-type.config.folderwatcher.ftpfolder.connectionTimeout.label = Connection Timeout
thing-type.config.folderwatcher.ftpfolder.connectionTimeout.description = Connection timeout for FTP request, sec
thing-type.config.folderwatcher.ftpfolder.connectionTimeout.description = Connection timeout for FTP request, in seconds
thing-type.config.folderwatcher.ftpfolder.diffHours.label = Timestamp Difference
thing-type.config.folderwatcher.ftpfolder.diffHours.description = How many hours back to analyze
thing-type.config.folderwatcher.ftpfolder.ftpAddress.label = FTP Server
@ -31,7 +33,7 @@ thing-type.config.folderwatcher.ftpfolder.listHidden.description = Allow listing
thing-type.config.folderwatcher.ftpfolder.listRecursiveFtp.label = List Sub Folders
thing-type.config.folderwatcher.ftpfolder.listRecursiveFtp.description = Allow listing of sub folders
thing-type.config.folderwatcher.ftpfolder.pollInterval.label = Polling Interval
thing-type.config.folderwatcher.ftpfolder.pollInterval.description = Interval for polling folder changes, sec
thing-type.config.folderwatcher.ftpfolder.pollInterval.description = Interval for polling folder changes, in seconds
thing-type.config.folderwatcher.ftpfolder.secureMode.label = FTP Security
thing-type.config.folderwatcher.ftpfolder.secureMode.description = FTP Security settings
thing-type.config.folderwatcher.ftpfolder.secureMode.option.NONE = None
@ -44,7 +46,21 @@ thing-type.config.folderwatcher.localfolder.listRecursiveLocal.description = All
thing-type.config.folderwatcher.localfolder.localDir.label = Local Directory
thing-type.config.folderwatcher.localfolder.localDir.description = Local directory to be watched
thing-type.config.folderwatcher.localfolder.pollIntervalLocal.label = Polling Interval
thing-type.config.folderwatcher.localfolder.pollIntervalLocal.description = Interval for polling folder changes, sec
thing-type.config.folderwatcher.localfolder.pollIntervalLocal.description = Interval for polling folder changes, in seconds
thing-type.config.folderwatcher.s3bucket.awsKey.label = AWS Access Key
thing-type.config.folderwatcher.s3bucket.awsKey.description = AWS access key
thing-type.config.folderwatcher.s3bucket.awsRegion.label = AWS Region
thing-type.config.folderwatcher.s3bucket.awsRegion.description = AWS region of S3 bucket
thing-type.config.folderwatcher.s3bucket.awsSecret.label = AWS Secret
thing-type.config.folderwatcher.s3bucket.awsSecret.description = AWS secret
thing-type.config.folderwatcher.s3bucket.pollIntervalS3.label = Polling Interval
thing-type.config.folderwatcher.s3bucket.pollIntervalS3.description = Interval for polling S3 bucket changes, in seconds
thing-type.config.folderwatcher.s3bucket.s3Anonymous.label = Anonymous Connection
thing-type.config.folderwatcher.s3bucket.s3Anonymous.description = Connect anonymously (works for public buckets)
thing-type.config.folderwatcher.s3bucket.s3BucketName.label = S3 Bucket Name
thing-type.config.folderwatcher.s3bucket.s3BucketName.description = Name of the S3 bucket to be watched
thing-type.config.folderwatcher.s3bucket.s3Path.label = S3 Path
thing-type.config.folderwatcher.s3bucket.s3Path.description = S3 path (folder) to be monitored
# channel types

View File

@ -62,13 +62,13 @@
</parameter>
<parameter name="connectionTimeout" type="integer" min="1" unit="s">
<label>Connection Timeout</label>
<description>Connection timeout for FTP request, sec</description>
<description>Connection timeout for FTP request, in seconds</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
<parameter name="pollInterval" type="integer" min="1" unit="s">
<label>Polling Interval</label>
<description>Interval for polling folder changes, sec</description>
<description>Interval for polling folder changes, in seconds</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
@ -105,7 +105,7 @@
</parameter>
<parameter name="pollIntervalLocal" type="integer" min="1" unit="s">
<label>Polling Interval</label>
<description>Interval for polling folder changes, sec</description>
<description>Interval for polling folder changes, in seconds</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
@ -123,4 +123,50 @@
</parameter>
</config-description>
</thing-type>
<thing-type id="s3bucket">
<label>AWS S3 Bucket</label>
<description>AWS S3 bucket to be watched</description>
<channels>
<channel id="newfile" typeId="newfile-channel"/>
</channels>
<config-description>
<parameter name="s3BucketName" type="text" required="true">
<label>S3 Bucket Name</label>
<description>Name of the S3 bucket to be watched</description>
</parameter>
<parameter name="s3Path" type="text">
<label>S3 Path</label>
<description>S3 path (folder) to be monitored</description>
</parameter>
<parameter name="awsRegion" type="text" required="true">
<label>AWS Region</label>
<description>AWS region of S3 bucket</description>
</parameter>
<parameter name="pollIntervalS3" type="integer" min="1" unit="s">
<label>Polling Interval</label>
<description>Interval for polling S3 bucket changes, in seconds</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="s3Anonymous" type="boolean">
<label>Anonymous Connection</label>
<default>false</default>
<description>Connect anonymously (works for public buckets)</description>
<advanced>true</advanced>
</parameter>
<parameter name="awsKey" type="text">
<label>AWS Access Key</label>
<description>AWS access key</description>
<advanced>true</advanced>
</parameter>
<parameter name="awsSecret" type="text">
<label>AWS Secret</label>
<description>AWS secret</description>
<context>password</context>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>