Add Azure Thing

Signed-off-by: Alexandr Salamatov <goopilot@gmail.com>
pull/14926/head
Alexandr Salamatov 2023-05-02 16:11:14 -05:00
parent a1fc3632e6
commit 1a84167b37
11 changed files with 686 additions and 134 deletions

View File

@ -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_LOCALFOLDER = new ThingTypeUID(BINDING_ID, "localfolder");
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";
}

View File

@ -18,6 +18,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.LocalFolderWatcherHandler;
import org.openhab.binding.folderwatcher.internal.handler.S3BucketWatcherHandler;
@ -42,7 +43,7 @@ import org.osgi.service.component.annotations.Reference;
public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
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;
@Activate
@ -65,6 +66,8 @@ public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
return new LocalFolderWatcherHandler(thing);
} else if (THING_TYPE_S3BUCKET.equals(thingTypeUID)) {
return new S3BucketWatcherHandler(thing, httpClientFactory);
} else if (THING_TYPE_AZUREBLOB.equals(thingTypeUID)) {
return new AzureBlobWatcherHandler(thing, httpClientFactory);
}
return null;
}

View File

@ -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;
}
}

View File

@ -13,25 +13,13 @@
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.exception.AuthException;
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.HttpUtils;
/**
* 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
*/
@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 ALGORITHM = "HMAC-SHA256";
// public static final String ALGORITHM = "HMAC-SHA256";
public static final String TERMINATOR = "aws4_request";
public static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
public static final String DATESTRING_FORMAT = "yyyyMMdd";
protected static String PAIR_SEPARATOR = "&";
protected static String VALEU_SEPARATOR = "=";
protected URL endpointUrl;
protected String httpMethod;
protected String serviceName;
@ -58,6 +46,9 @@ public abstract class AWS4SignerBase {
protected final SimpleDateFormat dateStampFormat;
public AWS4SignerBase(URL endpointUrl, String httpMethod, String serviceName, String regionName) {
super(PAIR_SEPARATOR, VALEU_SEPARATOR);
this.endpointUrl = endpointUrl;
this.httpMethod = httpMethod;
this.serviceName = serviceName;
@ -69,124 +60,15 @@ public abstract class AWS4SignerBase {
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,
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) throws HttpUtilException {
return httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters + "\n"
+ 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,
String canonicalRequest) throws AuthException {
return scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
+ 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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 = "";
}

View File

@ -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;
}
}
}

View File

@ -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.s3Path.label = S3 Path
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

View File

@ -4,6 +4,14 @@
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">
<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">
<label>FTP Folder</label>
<description>FTP folder to be watched</description>
@ -82,14 +90,6 @@
</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">
<label>Local Folder</label>
<description>Local folder to be watched</description>
@ -169,4 +169,42 @@
</parameter>
</config-description>
</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>