]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Folderwatcher] AWS S3 buckets monitoring support (#14669)
authorgoopilot <40123561+goopilot@users.noreply.github.com>
Sat, 15 Apr 2023 19:22:01 +0000 (14:22 -0500)
committerGitHub <noreply@github.com>
Sat, 15 Apr 2023 19:22:01 +0000 (21:22 +0200)
* Add S3 Thing

Signed-off-by: Alexandr Salamatov <goopilot@gmail.com>
14 files changed:
bundles/org.openhab.binding.folderwatcher/README.md
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/S3Actions.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerBase.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerForAuthorizationHeader.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/BinaryUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/HttpUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/S3BucketWatcherConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/S3BucketWatcherHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/i18n/folderwatcher.properties
bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml

index 6d9d9b4b0c44746f20b7a7026a2b4febfae20315..1fb0aad0bb6df5a2d688c86aa4e24dccb6c2d58a 100644 (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())
+
+end
+```
+
+S3 bucket example:
+
+```java
+rule "New S3 file"
+when 
+    Channel "folderwatcher:s3bucket:myS3bucket:newfile" triggered
 then
 
-    logInfo('NewLocalFile', receivedEvent.toString())
+    logInfo("NewS3File", receivedEvent.toString())
 
 end
 ```
index 4e2b42d23c9d6a2df4df55e1d601538e096755a5..a9b0d3d29653963131ca85166da3d1f69539f7f2 100644 (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";
 }
index ed37512cce1e8134b94e746ad3cdeda452cfce69..53c52636338009fe9713885fff242e3069c341ad 100644 (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;
     }
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/S3Actions.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/S3Actions.java
new file mode 100644 (file)
index 0000000..c005092
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerBase.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerBase.java
new file mode 100644 (file)
index 0000000..53c8cac
--- /dev/null
@@ -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);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerForAuthorizationHeader.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/auth/AWS4SignerForAuthorizationHeader.java
new file mode 100644 (file)
index 0000000..17a73a8
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/BinaryUtils.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/BinaryUtils.java
new file mode 100644 (file)
index 0000000..e2da533
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/HttpUtils.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/api/util/HttpUtils.java
new file mode 100644 (file)
index 0000000..bd323a2
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/S3BucketWatcherConfiguration.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/S3BucketWatcherConfiguration.java
new file mode 100644 (file)
index 0000000..ecba148
--- /dev/null
@@ -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 = "";
+}
index e057def94a281be897315d092feef763f63dc3b3..88d9f668842c7a9d55ed6bce61f3343cf8049929 100644 (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 {
index 5cebce63987b18acb3e9f43616793ff5dedab0aa..2113b08785e69f8538762dcd78b7745cb675a978 100644 (file)
@@ -102,6 +102,7 @@ public class LocalFolderWatcherHandler extends BaseThingHandler {
         ScheduledFuture<?> executionJob = this.executionJob;
         if (executionJob != null) {
             executionJob.cancel(true);
+            this.executionJob = null;
         }
     }
 
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/S3BucketWatcherHandler.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/S3BucketWatcherHandler.java
new file mode 100644 (file)
index 0000000..f7e7148
--- /dev/null
@@ -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;
+        }
+    }
+}
index 5bcc4a0f43042bc55c575d3962873be6d9a71185..15496e68832e1127c457b40aca7bec0283f96863 100644 (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
 
index 1a9dc7d18f1fc1d33ebd3c7ad12e5cdf916d6d4a..825acecb916d72c6a0ade297564d32eebc4052c9 100644 (file)
                        </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>
                        </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>
                        </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>