13 changed files with 388 additions and 172 deletions
@ -1,68 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.cache.ota; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.cache.CacheManager; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE; |
|||
|
|||
@Service |
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
|||
@RequiredArgsConstructor |
|||
public class CaffeineOtaPackageCache implements OtaPackageDataCache { |
|||
|
|||
private final CacheManager cacheManager; |
|||
|
|||
@Override |
|||
public byte[] get(String key) { |
|||
return get(key, 0, 0); |
|||
} |
|||
|
|||
@Override |
|||
public byte[] get(String key, int chunkSize, int chunk) { |
|||
byte[] data = cacheManager.getCache(OTA_PACKAGE_DATA_CACHE).get(key, byte[].class); |
|||
|
|||
if (chunkSize < 1) { |
|||
return data; |
|||
} |
|||
|
|||
if (data != null && data.length > 0) { |
|||
int startIndex = chunkSize * chunk; |
|||
|
|||
int size = Math.min(data.length - startIndex, chunkSize); |
|||
|
|||
if (startIndex < data.length && size > 0) { |
|||
byte[] result = new byte[size]; |
|||
System.arraycopy(data, startIndex, result, 0, size); |
|||
return result; |
|||
} |
|||
} |
|||
return new byte[0]; |
|||
} |
|||
|
|||
@Override |
|||
public void put(String key, byte[] value) { |
|||
cacheManager.getCache(OTA_PACKAGE_DATA_CACHE).putIfAbsent(key, value); |
|||
} |
|||
|
|||
@Override |
|||
public void evict(String key) { |
|||
cacheManager.getCache(OTA_PACKAGE_DATA_CACHE).evict(key); |
|||
} |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.cache.ota; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.data.redis.connection.RedisConnection; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE; |
|||
|
|||
@Service |
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
|||
@RequiredArgsConstructor |
|||
public class RedisOtaPackageDataCache implements OtaPackageDataCache { |
|||
|
|||
private final RedisConnectionFactory redisConnectionFactory; |
|||
|
|||
@Override |
|||
public byte[] get(String key) { |
|||
return get(key, 0, 0); |
|||
} |
|||
|
|||
@Override |
|||
public byte[] get(String key, int chunkSize, int chunk) { |
|||
try (RedisConnection connection = redisConnectionFactory.getConnection()) { |
|||
if (chunkSize == 0) { |
|||
return connection.get(toOtaPackageCacheKey(key)); |
|||
} |
|||
|
|||
int startIndex = chunkSize * chunk; |
|||
int endIndex = startIndex + chunkSize - 1; |
|||
return connection.getRange(toOtaPackageCacheKey(key), startIndex, endIndex); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void put(String key, byte[] value) { |
|||
try (RedisConnection connection = redisConnectionFactory.getConnection()) { |
|||
connection.set(toOtaPackageCacheKey(key), value); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void evict(String key) { |
|||
try (RedisConnection connection = redisConnectionFactory.getConnection()) { |
|||
connection.del(toOtaPackageCacheKey(key)); |
|||
} |
|||
} |
|||
|
|||
private byte[] toOtaPackageCacheKey(String key) { |
|||
return String.format("%s::%s", OTA_PACKAGE_DATA_CACHE, key).getBytes(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.mqtt.ota; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.OtaPackageId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.ota.OtaPackageType; |
|||
|
|||
@Data |
|||
public class OtaPackageShortInfo { |
|||
|
|||
private final TenantId tenantId; |
|||
private final OtaPackageId id; |
|||
private final OtaPackageType type; |
|||
private final long size; |
|||
|
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.transport.ota; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.SettableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.io.FileUtils; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.cache.ota.files.OtaFileState; |
|||
import org.thingsboard.server.common.data.id.OtaPackageId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.TransportServiceCallback; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; |
|||
import org.thingsboard.server.queue.util.TbTransportComponent; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import java.io.File; |
|||
import java.io.IOException; |
|||
import java.nio.file.Paths; |
|||
import java.util.Arrays; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.locks.Lock; |
|||
|
|||
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@TbTransportComponent |
|||
@RequiredArgsConstructor |
|||
public class DefaultTransportOtaPackageService implements TransportOtaPackageService { |
|||
|
|||
private final static String FILE_NAME_TEMPLATE = "%s.tmp"; |
|||
|
|||
@Value("${files.temporary_files_directory:}") |
|||
private String tmpDir; |
|||
@Value("${java.io.tmpdir}") |
|||
private String defaultTmpDir; |
|||
|
|||
private final ConcurrentMap<OtaPackageId, TransportOtaFileState> files = new ConcurrentHashMap<>(); |
|||
private final TbServiceInfoProvider serviceInfoProvider; |
|||
private final TransportService transportService; |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
if (StringUtils.isEmpty(tmpDir)) { |
|||
tmpDir = defaultTmpDir; |
|||
} |
|||
createTempDirectoryIfNotExist(); |
|||
cleanDirectory(); |
|||
log.info("Directory {} with temporary ota files cleaned", tmpDir); |
|||
} |
|||
|
|||
@SuppressWarnings("UnstableApiUsage") |
|||
@Override |
|||
public ListenableFuture<byte[]> get(TenantId tenantId, OtaPackageId otaId, int chunkSize, int chunk) { |
|||
var state = files.computeIfAbsent(otaId, tmp -> |
|||
new TransportOtaFileState(tenantId, otaId, Paths.get(tmpDir, "tb-transport", "ota", |
|||
String.format(FILE_NAME_TEMPLATE, otaId.getId().toString())))); |
|||
Lock lock = state.getLock(); |
|||
lock.lock(); |
|||
try { |
|||
if (state.isLoaded() && state.exists()) { |
|||
return readChunk(state, chunkSize, chunk); |
|||
} |
|||
if (state.getLoadFuture() == null) { |
|||
state.setLoaded(false); |
|||
state.setLoadFuture(requestFile(state)); |
|||
} |
|||
|
|||
state.updateLastActivityTime(); |
|||
return Futures.transformAsync(state.getLoadFuture(), tmp -> readChunk(state, chunkSize, chunk), transportService.getCallbackExecutor()); |
|||
} finally { |
|||
lock.unlock(); |
|||
} |
|||
} |
|||
|
|||
private SettableFuture<Void> requestFile(TransportOtaFileState state) { |
|||
SettableFuture<Void> result = SettableFuture.create(); |
|||
transportService.process(TransportProtos.SendOtaPackageBodyRequestMsg.newBuilder() |
|||
.setTenantIdMSB(state.getTenantId().getId().getMostSignificantBits()) |
|||
.setTenantIdLSB(state.getTenantId().getId().getLeastSignificantBits()) |
|||
.setOtaPackageIdMSB(state.getOtaId().getId().getMostSignificantBits()) |
|||
.setOtaPackageIdMSB(state.getOtaId().getId().getLeastSignificantBits()) |
|||
.setServiceId(serviceInfoProvider.getServiceId()) |
|||
.build(), new TransportServiceCallback<>() { |
|||
@Override |
|||
public void onSuccess(TransportProtos.SendOtaPackageBodyResponseMsg msg) { |
|||
if (msg.getResponseStatus() == TransportProtos.ResponseStatus.SUCCESS) { |
|||
log.debug("[{}] Requested ota file content", state.getOtaId()); |
|||
} else { |
|||
log.warn("[{}] Failed to request ota file content due to: {}", state.getOtaId(), msg.getResponseStatus()); |
|||
state.getLoadFuture().setException(new RuntimeException("Failed to request ota file content due to: " + msg.getResponseStatus())); |
|||
} |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void onError(Throwable e) { |
|||
log.warn("[{}] Failed to request ota file content", state.getOtaId(), e); |
|||
state.getLoadFuture().setException(e); |
|||
} |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
private ListenableFuture<byte[]> readChunk(TransportOtaFileState state, int chunkSize, int chunk) { |
|||
//TODO: implement;
|
|||
// byte[] data = cacheManager.getCache(OTA_PACKAGE_DATA_CACHE).get(key, byte[].class);
|
|||
//
|
|||
// if (chunkSize < 1) {
|
|||
// return data;
|
|||
// }
|
|||
//
|
|||
// if (data != null && data.length > 0) {
|
|||
// int startIndex = chunkSize * chunk;
|
|||
//
|
|||
// int size = Math.min(data.length - startIndex, chunkSize);
|
|||
//
|
|||
// if (startIndex < data.length && size > 0) {
|
|||
// byte[] result = new byte[size];
|
|||
// System.arraycopy(data, startIndex, result, 0, size);
|
|||
// return result;
|
|||
// }
|
|||
// }
|
|||
// return new byte[0];
|
|||
|
|||
return Futures.immediateFuture(null); |
|||
} |
|||
|
|||
private void createTempDirectoryIfNotExist() { |
|||
File directory = Paths.get(tmpDir, "tb-transport", "ota").toFile(); |
|||
if (!directory.exists()) { |
|||
try { |
|||
FileUtils.forceMkdir(directory); |
|||
} catch (IOException e) { |
|||
log.error("Failed to create directory for temporary files ", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void cleanDirectory() { |
|||
File directory = Paths.get(tmpDir, "tb-transport", "ota").toFile(); |
|||
if (directory.isDirectory()) { |
|||
File[] files = directory.listFiles(); |
|||
if (files == null) return; |
|||
Arrays.stream(files).forEach( |
|||
file -> { |
|||
try { |
|||
FileUtils.delete(file); |
|||
} catch (Exception e) { |
|||
log.error("Failed to delete file {}", file.getName(), e); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.transport.ota; |
|||
|
|||
import com.google.common.util.concurrent.SettableFuture; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.OtaPackageId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
import java.io.File; |
|||
import java.nio.file.Path; |
|||
import java.util.concurrent.locks.Lock; |
|||
import java.util.concurrent.locks.ReentrantLock; |
|||
|
|||
@Data |
|||
public class TransportOtaFileState { |
|||
|
|||
private final Lock lock = new ReentrantLock(); |
|||
private final TenantId tenantId; |
|||
private final OtaPackageId otaId; |
|||
private final Path filePath; |
|||
|
|||
private boolean loaded; |
|||
private SettableFuture<Void> loadFuture; |
|||
|
|||
private long lastActivityTime; |
|||
|
|||
public void updateLastActivityTime() { |
|||
lastActivityTime = System.currentTimeMillis(); |
|||
} |
|||
|
|||
public boolean exists() { |
|||
return filePath.toFile().exists(); |
|||
} |
|||
|
|||
public File getFile() { |
|||
return filePath.toFile(); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue