Add Azure Thing
Signed-off-by: Alexandr Salamatov <goopilot@gmail.com>pull/14926/head
parent
a1fc3632e6
commit
1a84167b37
|
@ -27,5 +27,6 @@ public class FolderWatcherBindingConstants {
|
||||||
public static final ThingTypeUID THING_TYPE_FTPFOLDER = new ThingTypeUID(BINDING_ID, "ftpfolder");
|
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_LOCALFOLDER = new ThingTypeUID(BINDING_ID, "localfolder");
|
||||||
public static final ThingTypeUID THING_TYPE_S3BUCKET = new ThingTypeUID(BINDING_ID, "s3bucket");
|
public static final ThingTypeUID THING_TYPE_S3BUCKET = new ThingTypeUID(BINDING_ID, "s3bucket");
|
||||||
|
public static final ThingTypeUID THING_TYPE_AZUREBLOB = new ThingTypeUID(BINDING_ID, "azureblob");
|
||||||
public static final String CHANNEL_NEWFILE = "newfile";
|
public static final String CHANNEL_NEWFILE = "newfile";
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.handler.AzureBlobWatcherHandler;
|
||||||
import org.openhab.binding.folderwatcher.internal.handler.FtpFolderWatcherHandler;
|
import org.openhab.binding.folderwatcher.internal.handler.FtpFolderWatcherHandler;
|
||||||
import org.openhab.binding.folderwatcher.internal.handler.LocalFolderWatcherHandler;
|
import org.openhab.binding.folderwatcher.internal.handler.LocalFolderWatcherHandler;
|
||||||
import org.openhab.binding.folderwatcher.internal.handler.S3BucketWatcherHandler;
|
import org.openhab.binding.folderwatcher.internal.handler.S3BucketWatcherHandler;
|
||||||
|
@ -42,7 +43,7 @@ import org.osgi.service.component.annotations.Reference;
|
||||||
public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
|
public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_FTPFOLDER,
|
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_FTPFOLDER,
|
||||||
THING_TYPE_LOCALFOLDER, THING_TYPE_S3BUCKET);
|
THING_TYPE_LOCALFOLDER, THING_TYPE_S3BUCKET, THING_TYPE_AZUREBLOB);
|
||||||
private HttpClientFactory httpClientFactory;
|
private HttpClientFactory httpClientFactory;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
|
@ -65,6 +66,8 @@ public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
|
||||||
return new LocalFolderWatcherHandler(thing);
|
return new LocalFolderWatcherHandler(thing);
|
||||||
} else if (THING_TYPE_S3BUCKET.equals(thingTypeUID)) {
|
} else if (THING_TYPE_S3BUCKET.equals(thingTypeUID)) {
|
||||||
return new S3BucketWatcherHandler(thing, httpClientFactory);
|
return new S3BucketWatcherHandler(thing, httpClientFactory);
|
||||||
|
} else if (THING_TYPE_AZUREBLOB.equals(thingTypeUID)) {
|
||||||
|
return new AzureBlobWatcherHandler(thing, httpClientFactory);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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.Azure4SignerForAuthorizationHeader;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
||||||
|
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 AzureActions} class contains AWS S3 API implementation.
|
||||||
|
*
|
||||||
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AzureActions {
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
private static final Duration REQUEST_TIMEOUT = Duration.ofMinutes(1);
|
||||||
|
private static final String CONTENT_TYPE = "application/xml";
|
||||||
|
private URL containerUri;
|
||||||
|
private String azureAccessKey;
|
||||||
|
private String accountName;
|
||||||
|
private String containerName;
|
||||||
|
|
||||||
|
public AzureActions(HttpClientFactory httpClientFactory, String accountName, String containerName) {
|
||||||
|
this(httpClientFactory, accountName, containerName, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public AzureActions(HttpClientFactory httpClientFactory, String accountName, String containerName,
|
||||||
|
String azureAccessKey) {
|
||||||
|
this.httpClient = httpClientFactory.getCommonHttpClient();
|
||||||
|
try {
|
||||||
|
this.containerUri = new URL("https://" + accountName + ".blob.core.windows.net/" + containerName);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
|
||||||
|
}
|
||||||
|
this.azureAccessKey = azureAccessKey;
|
||||||
|
this.accountName = accountName;
|
||||||
|
this.containerName = containerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> listContainer(String prefix) throws Exception {
|
||||||
|
Map<String, String> headers = new HashMap<String, String>();
|
||||||
|
Map<String, String> params = new HashMap<String, String>();
|
||||||
|
return listBlob(prefix, headers, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> listBlob(String prefix, Map<String, String> headers, Map<String, String> params)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
params.put("restype", "container");
|
||||||
|
params.put("comp", "list");
|
||||||
|
params.put("maxresults", "1000");
|
||||||
|
params.put("prefix", prefix);
|
||||||
|
headers.put(ACCEPT.toString(), CONTENT_TYPE);
|
||||||
|
|
||||||
|
if (!azureAccessKey.isEmpty()) {
|
||||||
|
Azure4SignerForAuthorizationHeader signer = new Azure4SignerForAuthorizationHeader("GET",
|
||||||
|
this.containerUri);
|
||||||
|
String authorization;
|
||||||
|
try {
|
||||||
|
authorization = signer.computeSignature(headers, params, accountName, azureAccessKey, containerName);
|
||||||
|
} catch (HttpUtilException e) {
|
||||||
|
throw new AuthException(e);
|
||||||
|
}
|
||||||
|
headers.put("Authorization", authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = httpClient.newRequest(this.containerUri.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();
|
||||||
|
// This returns extra character before <xml. Need to find out why
|
||||||
|
String sResponse = contentResponse.getContentAsString();
|
||||||
|
InputSource is = new InputSource(new StringReader(sResponse.substring(sResponse.indexOf("<"))));
|
||||||
|
Document doc = docBuilder.parse(is);
|
||||||
|
NodeList nameNodesList = doc.getElementsByTagName("Blob");
|
||||||
|
List<String> returnList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (nameNodesList.getLength() == 0) {
|
||||||
|
return returnList;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < nameNodesList.getLength(); i++) {
|
||||||
|
returnList.add(nameNodesList.item(i).getFirstChild().getTextContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
nameNodesList = doc.getElementsByTagName("NextMarker");
|
||||||
|
if (nameNodesList.getLength() > 0) {
|
||||||
|
if (nameNodesList.item(0).getChildNodes().getLength() > 0) {
|
||||||
|
String continueToken = nameNodesList.item(0).getFirstChild().getTextContent();
|
||||||
|
params.clear();
|
||||||
|
headers.clear();
|
||||||
|
params.put("marker", continueToken);
|
||||||
|
returnList.addAll(listBlob(prefix, headers, params));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnList;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,25 +13,13 @@
|
||||||
package org.openhab.binding.folderwatcher.internal.api.auth;
|
package org.openhab.binding.folderwatcher.internal.api.auth;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.text.SimpleDateFormat;
|
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.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.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
|
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
|
||||||
import org.openhab.binding.folderwatcher.internal.api.util.BinaryUtils;
|
import org.openhab.binding.folderwatcher.internal.api.util.BinaryUtils;
|
||||||
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
||||||
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AWS4SignerBase} class contains based methods for AWS S3 API authentication.
|
* The {@link AWS4SignerBase} class contains based methods for AWS S3 API authentication.
|
||||||
|
@ -41,15 +29,15 @@ import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;
|
||||||
* @author Alexandr Salamatov - Initial contribution
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public abstract class AWS4SignerBase {
|
public abstract class AWS4SignerBase extends SignerBase {
|
||||||
|
|
||||||
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 SCHEME = "AWS4";
|
||||||
public static final String ALGORITHM = "HMAC-SHA256";
|
// public static final String ALGORITHM = "HMAC-SHA256";
|
||||||
public static final String TERMINATOR = "aws4_request";
|
public static final String TERMINATOR = "aws4_request";
|
||||||
public static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
|
public static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
|
||||||
public static final String DATESTRING_FORMAT = "yyyyMMdd";
|
public static final String DATESTRING_FORMAT = "yyyyMMdd";
|
||||||
|
protected static String PAIR_SEPARATOR = "&";
|
||||||
|
protected static String VALEU_SEPARATOR = "=";
|
||||||
protected URL endpointUrl;
|
protected URL endpointUrl;
|
||||||
protected String httpMethod;
|
protected String httpMethod;
|
||||||
protected String serviceName;
|
protected String serviceName;
|
||||||
|
@ -58,6 +46,9 @@ public abstract class AWS4SignerBase {
|
||||||
protected final SimpleDateFormat dateStampFormat;
|
protected final SimpleDateFormat dateStampFormat;
|
||||||
|
|
||||||
public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
|
public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
|
||||||
|
|
||||||
|
super(PAIR_SEPARATOR, VALEU_SEPARATOR);
|
||||||
|
|
||||||
this.endpointUrl = endpointUrl;
|
this.endpointUrl = endpointUrl;
|
||||||
this.httpMethod = httpMethod;
|
this.httpMethod = httpMethod;
|
||||||
this.serviceName = serviceName;
|
this.serviceName = serviceName;
|
||||||
|
@ -69,124 +60,15 @@ public abstract class AWS4SignerBase {
|
||||||
dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
|
dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static String getCanonicalizeHeaderNames(Map<String, String> headers) {
|
|
||||||
List<String> sortedHeaders = new ArrayList<>();
|
|
||||||
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<>();
|
|
||||||
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,
|
protected static String getCanonicalRequest(URL endpoint, String httpMethod, String queryParameters,
|
||||||
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) throws HttpUtilException {
|
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) throws HttpUtilException {
|
||||||
return httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters + "\n"
|
return httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters + "\n"
|
||||||
+ canonicalizedHeaders + "\n" + canonicalizedHeaderNames + "\n" + bodyHash;
|
+ canonicalizedHeaders + "\n" + canonicalizedHeaderNames + "\n" + bodyHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static String getCanonicalizedResourcePath(URL endpoint) throws HttpUtilException {
|
|
||||||
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) throws HttpUtilException {
|
|
||||||
if (parameters == null || parameters.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
SortedMap<String, String> sorted = new TreeMap<>();
|
|
||||||
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,
|
protected static String getStringToSign(String scheme, String algorithm, String dateTime, String scope,
|
||||||
String canonicalRequest) throws AuthException {
|
String canonicalRequest) throws AuthException {
|
||||||
return scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
|
return scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
|
||||||
+ BinaryUtils.toHex(hash(canonicalRequest));
|
+ BinaryUtils.toHex(hash(canonicalRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] hash(String text) throws AuthException {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
md.update(text.getBytes("UTF-8"));
|
|
||||||
return md.digest();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] hash(byte[] data) throws AuthException {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
md.update(data);
|
|
||||||
return md.digest();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static byte[] sign(String stringData, byte[] key, String algorithm) throws AuthException {
|
|
||||||
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 AuthException("Unable to calculate a request signature: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Azure4SignerForAuthorizationHeader} class contains methods for Azure Blob API authentication using HTTPS
|
||||||
|
* headers.
|
||||||
|
*
|
||||||
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class Azure4SignerForAuthorizationHeader extends AzureSignerBase {
|
||||||
|
|
||||||
|
public Azure4SignerForAuthorizationHeader(String httpMethod, URL endpointUrl) {
|
||||||
|
super(endpointUrl, httpMethod, "serviceName", "regionName");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String computeSignature(Map<String, String> headers, Map<String, String> queryParameters,
|
||||||
|
String AzureAccount, String AzureSecretKey, String azureContainerName)
|
||||||
|
throws AuthException, HttpUtilException {
|
||||||
|
String dateTimeStamp = dateTimeFormat.format(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC));
|
||||||
|
headers.put("x-ms-date", dateTimeStamp);
|
||||||
|
headers.put("x-ms-version", "2020-08-04");
|
||||||
|
|
||||||
|
Map<String, String> filteredHeaders = new HashMap<>();
|
||||||
|
filteredHeaders.putAll(headers);
|
||||||
|
Iterator<Map.Entry<String, String>> iterator = filteredHeaders.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, String> entry = iterator.next();
|
||||||
|
if (!entry.getKey().startsWith(HEADER_FILTER)) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String canonicalizedHeaders = getCanonicalizedHeaderString(filteredHeaders);
|
||||||
|
String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
|
||||||
|
String canonicalilezdResource = getCanonicalResource(endpointUrl, canonicalizedQueryParameters);
|
||||||
|
String stringToSign = getStringToSign("GET", "", "", "", "", "", "", "", "", "", "", "", canonicalizedHeaders,
|
||||||
|
canonicalilezdResource);
|
||||||
|
byte[] kSecret = Base64.getDecoder().decode(AzureSecretKey);
|
||||||
|
byte[] signed = sign(stringToSign, kSecret, "HmacSHA256");
|
||||||
|
return "SharedKey" + " " + AzureAccount + ":" + Base64.getEncoder().encodeToString(signed);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.exception.AuthException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link AzureSignerBase} class contains based methods for Azure Blob API authentication.
|
||||||
|
*
|
||||||
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public abstract class AzureSignerBase extends SignerBase {
|
||||||
|
protected static String PAIR_SEPARATOR = "\n";
|
||||||
|
protected static String VALEU_SEPARATOR = ":";
|
||||||
|
protected static String HEADER_FILTER = "x-ms-";
|
||||||
|
protected URL endpointUrl;
|
||||||
|
protected String httpMethod;
|
||||||
|
protected String serviceName;
|
||||||
|
protected String regionName;
|
||||||
|
protected DateTimeFormatter dateTimeFormat;
|
||||||
|
|
||||||
|
public AzureSignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
|
||||||
|
|
||||||
|
super(PAIR_SEPARATOR, VALEU_SEPARATOR);
|
||||||
|
|
||||||
|
this.endpointUrl = endpointUrl;
|
||||||
|
this.httpMethod = httpMethod;
|
||||||
|
this.serviceName = serviceName;
|
||||||
|
this.regionName = regionName;
|
||||||
|
|
||||||
|
dateTimeFormat = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String getCanonicalResource(URL endpoint, String queryParameters) throws HttpUtilException {
|
||||||
|
return getCanonicalizedResourceName(endpoint) + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String getCanonicalizedResourceName(URL endpoint) throws HttpUtilException {
|
||||||
|
if (endpoint == null) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
String path = endpoint.getHost().split("\\.")[0];
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
String encodedPath = HttpUtils.urlEncode(path, true);
|
||||||
|
if (encodedPath.startsWith("/")) {
|
||||||
|
return encodedPath;
|
||||||
|
} else {
|
||||||
|
return "/".concat(encodedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String getStringToSign(String VERB, String ContentEncoding, String ContentLanguage,
|
||||||
|
String ContentLength, String ContentMD5, String ContentType, String Date, String IfModifiedSince,
|
||||||
|
String IfMatch, String IfNoneMatch, String IfUnmodifiedSince, String Range, String CanonicalizedHeaders,
|
||||||
|
String CanonicalizedResource) throws AuthException {
|
||||||
|
|
||||||
|
return VERB + "\n" + ContentEncoding + "\n" + ContentLanguage + "\n" + ContentLength + "\n" + ContentMD5 + "\n"
|
||||||
|
+ ContentType + "\n" + Date + "\n" + IfModifiedSince + "\n" + IfMatch + "\n" + IfNoneMatch + "\n"
|
||||||
|
+ IfUnmodifiedSince + "\n" + Range + "\n" + CanonicalizedHeaders + CanonicalizedResource;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.exception.AuthException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtilException;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.api.util.HttpUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link SignerBase} class contains based methods for 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 SignerBase {
|
||||||
|
|
||||||
|
public static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||||
|
public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
|
||||||
|
public static final String ALGORITHM = "HMAC-SHA256";
|
||||||
|
private static String PAIR_SEPARATOR = "&";
|
||||||
|
private static String VALUE_SEPARATOR = "=";
|
||||||
|
|
||||||
|
public SignerBase(String PAIRSEPARATOR, String VALUESEPARATOR) {
|
||||||
|
PAIR_SEPARATOR = PAIRSEPARATOR;
|
||||||
|
VALUE_SEPARATOR = VALUESEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String getCanonicalizeHeaderNames(Map<String, String> headers) {
|
||||||
|
List<String> sortedHeaders = new ArrayList<>();
|
||||||
|
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<>();
|
||||||
|
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 getCanonicalizedResourcePath(URL endpoint) throws HttpUtilException {
|
||||||
|
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) throws HttpUtilException {
|
||||||
|
if (parameters == null || parameters.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedMap<String, String> sorted = new TreeMap<>();
|
||||||
|
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(VALUE_SEPARATOR);
|
||||||
|
builder.append(pair.getValue());
|
||||||
|
if (pairs.hasNext()) {
|
||||||
|
builder.append(PAIR_SEPARATOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hash(String text) throws AuthException {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
md.update(text.getBytes("UTF-8"));
|
||||||
|
return md.digest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] hash(byte[] data) throws AuthException {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
md.update(data);
|
||||||
|
return md.digest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthException("Unable to compute hash while signing request: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static byte[] sign(String stringData, byte[] key, String algorithm) throws AuthException {
|
||||||
|
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 AuthException("Unable to calculate a request signature: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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 AzureBlobWatcherConfiguration} class contains fields mapping thing configuration parameters.
|
||||||
|
*
|
||||||
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AzureBlobWatcherConfiguration {
|
||||||
|
public String azureAccountName = "";
|
||||||
|
public String azureContainerName = "";
|
||||||
|
public boolean azureAnonymous;
|
||||||
|
public String azureAccessKey = "";
|
||||||
|
public int pollIntervalAzure;
|
||||||
|
public String contanerPath = "";
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 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.AzureActions;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.common.WatcherCommon;
|
||||||
|
import org.openhab.binding.folderwatcher.internal.config.AzureBlobWatcherConfiguration;
|
||||||
|
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 AzureBlobWatcherHandler} is responsible for handling commands, which are
|
||||||
|
* sent to one of the channels.
|
||||||
|
*
|
||||||
|
* @author Alexandr Salamatov - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AzureBlobWatcherHandler extends BaseThingHandler {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AzureBlobWatcherHandler.class);
|
||||||
|
private AzureBlobWatcherConfiguration config = new AzureBlobWatcherConfiguration();
|
||||||
|
private File currentBlobListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "AzureBlob"
|
||||||
|
+ File.separator + thing.getUID().getAsString().replace(':', '_') + ".data");
|
||||||
|
private @Nullable ScheduledFuture<?> executionJob;
|
||||||
|
private List<String> previousBlobListing = new ArrayList<>();
|
||||||
|
private HttpClientFactory httpClientFactory;
|
||||||
|
private @Nullable AzureActions azure;
|
||||||
|
|
||||||
|
public AzureBlobWatcherHandler(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) {
|
||||||
|
refreshAzureBlobInformation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
config = getConfigAs(AzureBlobWatcherConfiguration.class);
|
||||||
|
|
||||||
|
if (config.azureAnonymous) {
|
||||||
|
azure = new AzureActions(httpClientFactory, config.azureAccountName, config.azureContainerName);
|
||||||
|
} else {
|
||||||
|
azure = new AzureActions(httpClientFactory, config.azureAccountName, config.azureContainerName,
|
||||||
|
config.azureAccessKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
previousBlobListing = WatcherCommon.initStorage(currentBlobListingFile,
|
||||||
|
config.azureAccountName + "-" + config.azureContainerName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||||
|
logger.debug("Can't write file {}: {}", currentBlobListingFile, e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshAzureBlobInformation()) {
|
||||||
|
if (config.pollIntervalAzure > 0) {
|
||||||
|
executionJob = scheduler.scheduleWithFixedDelay(this::refreshAzureBlobInformation,
|
||||||
|
config.pollIntervalAzure, config.pollIntervalAzure, TimeUnit.SECONDS);
|
||||||
|
} else {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||||
|
"Polling interval must be greater then 0 seconds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean refreshAzureBlobInformation() {
|
||||||
|
List<String> currentBlobListing = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
currentBlobListing = azure.listContainer(config.contanerPath);
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
List<String> difBlobListing = new ArrayList<>(currentBlobListing);
|
||||||
|
difBlobListing.removeAll(previousBlobListing);
|
||||||
|
difBlobListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file));
|
||||||
|
|
||||||
|
if (!difBlobListing.isEmpty()) {
|
||||||
|
WatcherCommon.saveNewListing(difBlobListing, currentBlobListingFile);
|
||||||
|
}
|
||||||
|
previousBlobListing = new ArrayList<>(currentBlobListing);
|
||||||
|
} catch (Exception e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||||
|
"Can't connect to the contaner: " + e.getMessage());
|
||||||
|
logger.debug("Can't connect to the contaner: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
ScheduledFuture<?> executionJob = this.executionJob;
|
||||||
|
if (executionJob != null) {
|
||||||
|
executionJob.cancel(true);
|
||||||
|
this.executionJob = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,6 +61,18 @@ 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.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.label = S3 Path
|
||||||
thing-type.config.folderwatcher.s3bucket.s3Path.description = S3 path (folder) to be monitored
|
thing-type.config.folderwatcher.s3bucket.s3Path.description = S3 path (folder) to be monitored
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAccountName.label = Azure Account Name
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAccountName.description = Name of the Azure account where the conaner located
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureContainerName.label = Azure Blob Container Name
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureContainerName.description = Name of the Azure container to be watched
|
||||||
|
thing-type.config.folderwatcher.azureblob.contanerPath.label = Container Path
|
||||||
|
thing-type.config.folderwatcher.azureblob.contanerPath.description = Container path (folder) to be monitored
|
||||||
|
thing-type.config.folderwatcher.azureblob.pollIntervalAzure.label = Polling Interval
|
||||||
|
thing-type.config.folderwatcher.azureblob.pollIntervalAzure.description = Interval for polling Azure contaner changes, in seconds
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAnonymous.label = Anonymous Connection
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAnonymous.description = Connect anonymously (works for public containers)
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAccessKey.label = Azure Access Key
|
||||||
|
thing-type.config.folderwatcher.azureblob.azureAccessKey.description = Access Key for Azure storage account
|
||||||
|
|
||||||
# channel types
|
# channel types
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,14 @@
|
||||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
|
||||||
|
<channel-type id="newfile-channel">
|
||||||
|
<kind>trigger</kind>
|
||||||
|
<label>New File Name(s)</label>
|
||||||
|
<description>A new file name</description>
|
||||||
|
<category>String</category>
|
||||||
|
<event/>
|
||||||
|
</channel-type>
|
||||||
|
|
||||||
<thing-type id="ftpfolder">
|
<thing-type id="ftpfolder">
|
||||||
<label>FTP Folder</label>
|
<label>FTP Folder</label>
|
||||||
<description>FTP folder to be watched</description>
|
<description>FTP folder to be watched</description>
|
||||||
|
@ -82,14 +90,6 @@
|
||||||
|
|
||||||
</thing-type>
|
</thing-type>
|
||||||
|
|
||||||
<channel-type id="newfile-channel">
|
|
||||||
<kind>trigger</kind>
|
|
||||||
<label>New File Name(s)</label>
|
|
||||||
<description>A new file name</description>
|
|
||||||
<category>String</category>
|
|
||||||
<event/>
|
|
||||||
</channel-type>
|
|
||||||
|
|
||||||
<thing-type id="localfolder">
|
<thing-type id="localfolder">
|
||||||
<label>Local Folder</label>
|
<label>Local Folder</label>
|
||||||
<description>Local folder to be watched</description>
|
<description>Local folder to be watched</description>
|
||||||
|
@ -169,4 +169,42 @@
|
||||||
</parameter>
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
</thing-type>
|
</thing-type>
|
||||||
|
<thing-type id="azureblob">
|
||||||
|
<label>Azure Blob Storage</label>
|
||||||
|
<description>Azure Blob Storage to be watched</description>
|
||||||
|
|
||||||
|
<channels>
|
||||||
|
<channel id="newfile" typeId="newfile-channel"/>
|
||||||
|
</channels>
|
||||||
|
|
||||||
|
<config-description>
|
||||||
|
<parameter name="azureAccountName" type="text" required="true">
|
||||||
|
<label>Azure Account Name</label>
|
||||||
|
<description>Name of the Azure account where the conaner located</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="azureContainerName" type="text" required="true">
|
||||||
|
<label>Azure Blob Container Name</label>
|
||||||
|
<description>Name of the Azure container to be watched</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="contanerPath" type="text">
|
||||||
|
<label>Container Path</label>
|
||||||
|
<description>Container path (folder) to be monitored</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="pollIntervalAzure" type="integer" min="1" unit="s">
|
||||||
|
<label>Polling Interval</label>
|
||||||
|
<description>Interval for polling Azure contaner changes, in seconds</description>
|
||||||
|
<default>60</default>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="azureAnonymous" type="boolean">
|
||||||
|
<label>Anonymous Connection</label>
|
||||||
|
<default>false</default>
|
||||||
|
<description>Connect anonymously (works for public containers)</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="azureAccessKey" type="text">
|
||||||
|
<label>Azure Access Key</label>
|
||||||
|
<description>Access Key for Azure storage account</description>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</thing-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
|
Loading…
Reference in New Issue