From 0f290d98f70d178ce5b67060d4e7de57773683e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LAPTOP-S9HJSOEB=5C=E6=98=8A=E5=A4=A9?= Date: Mon, 29 Jul 2024 14:44:52 +0800 Subject: [PATCH] =?UTF-8?q?s3=E6=9A=82=E6=97=B6=E7=BC=96=E5=86=99=E5=9B=9E?= =?UTF-8?q?=E6=94=BE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../leaper/web/config/ConfigProperties.java | 12 ++ .../java/com/leaper/web/config/S3Config.java | 25 +++++ .../leaper/web/controller/CameraRecord.java | 103 +++++++++++++++++ .../leaper/web/entity/CameraRecordDuoji.java | 25 +++++ .../java/com/leaper/web/entity/Record.java | 17 +++ .../leaper/web/lib/CameraControlModule.java | 1 + .../lib/hik/HikCameraControlModuleImpl.java | 45 ++++++++ .../web/lib/hik/HikExceptionCallBack.java | 2 +- .../JoywareCameraControlModuleImpl.java | 4 +- .../web/mapper/CameraRecordDuojiMapper.java | 8 ++ .../com/leaper/web/service/CameraService.java | 8 +- .../java/com/leaper/web/service/CronTab.java | 104 +++++++++++++++--- .../java/com/leaper/web/task/S3Utils.java | 1 + web/src/main/java/com/leaper/web/task/T.java | 4 - web/src/main/resources/application-prod.yml | 9 +- 15 files changed, 343 insertions(+), 25 deletions(-) create mode 100644 web/src/main/java/com/leaper/web/config/S3Config.java create mode 100644 web/src/main/java/com/leaper/web/controller/CameraRecord.java create mode 100644 web/src/main/java/com/leaper/web/entity/CameraRecordDuoji.java create mode 100644 web/src/main/java/com/leaper/web/entity/Record.java create mode 100644 web/src/main/java/com/leaper/web/mapper/CameraRecordDuojiMapper.java delete mode 100644 web/src/main/java/com/leaper/web/task/T.java diff --git a/web/src/main/java/com/leaper/web/config/ConfigProperties.java b/web/src/main/java/com/leaper/web/config/ConfigProperties.java index 82b5f44..4c905a5 100644 --- a/web/src/main/java/com/leaper/web/config/ConfigProperties.java +++ b/web/src/main/java/com/leaper/web/config/ConfigProperties.java @@ -37,6 +37,18 @@ public class ConfigProperties { private ScanCodeMode scanCodeMode; private ServerOpenInfo serverOpenInfo; + private S3Config s3Config; + @Data + public static class S3Config { + + private String accessKey = "minio"; + private String secretKey = "minio123"; + // 桶所在的区域对应网关URL的域名地址 + private String host = "192.168.1.52:9000"; + private String bucketName = "camera"; + + private String urlCache = "D://camera/"; + } @Data diff --git a/web/src/main/java/com/leaper/web/config/S3Config.java b/web/src/main/java/com/leaper/web/config/S3Config.java new file mode 100644 index 0000000..5b4c687 --- /dev/null +++ b/web/src/main/java/com/leaper/web/config/S3Config.java @@ -0,0 +1,25 @@ +package com.leaper.web.config; + + +import com.leaper.web.task.S3Utils; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; + +// ftp配置 +@Configuration +public class S3Config { + + @Resource + ConfigProperties properties; + + @Bean + public S3Utils s3Utils() { + + S3Utils s3Utils = new S3Utils(); + s3Utils.init(properties.getS3Config().getAccessKey(), properties.getS3Config().getSecretKey(), properties.getS3Config().getHost()); + return s3Utils; + } +} diff --git a/web/src/main/java/com/leaper/web/controller/CameraRecord.java b/web/src/main/java/com/leaper/web/controller/CameraRecord.java new file mode 100644 index 0000000..97eadac --- /dev/null +++ b/web/src/main/java/com/leaper/web/controller/CameraRecord.java @@ -0,0 +1,103 @@ +package com.leaper.web.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.additional.query.impl.QueryChainWrapper; +import com.github.pagehelper.PageInfo; +import com.leaper.filter.pojo.LicenseHandler; +import com.leaper.web.config.ConfigProperties; +import com.leaper.web.entity.Camera; +import com.leaper.web.entity.CameraRecordDuoji; +import com.leaper.web.entity.Record; +import com.leaper.web.lib.CameraControlModule; +import com.leaper.web.lib.hik.HikCameraControlModuleImpl; +import com.leaper.web.mapper.CameraRecordDuojiMapper; +import com.leaper.web.pojo.street.StreetSearch; +import com.leaper.web.task.S3Utils; +import com.zhehekeji.core.pojo.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.io.File; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +@Api(value = "cameraRecord", tags = "球机回放管理") +@RestController +@RequestMapping("/cameraRecord") +@Slf4j +public class CameraRecord { + @Resource + CameraRecordDuojiMapper cameraRecordDuojiMapper; + @Resource + ConfigProperties configProperties; + + @Resource + S3Utils s3Utils; + + + @PostMapping("/record") + @ApiOperation(value = "回放列表 ") + @LicenseHandler + public Result record(@RequestBody Record record) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-dd"); + + LocalDate today = LocalDate.parse(record.getDate(), formatter); + + LocalDateTime startOfDay = LocalDateTime.of(today, LocalTime.MIN); + LocalDateTime endOfDay = LocalDateTime.of(today, LocalTime.MAX); + List cameraRecordDuoji = cameraRecordDuojiMapper.selectList(new QueryWrapper() + .eq("camera_Id", record.getCameraId()) + .ge("start_Time", startOfDay) + .le("end_Time", endOfDay) + .orderByAsc("start_Time")); + record.setStartTimeLong(0L); + record.setEndTimeLong(24L * 60 * 60); + List urls = cameraRecordDuoji.stream() + .map(cameraRecordDuoji1 -> { + Record r = new Record(); + r.setUrl( "/"+configProperties.getS3Config().getBucketName() +"/"+cameraRecordDuoji1.getPath() +".mp4"); + r.setStartTimeLong(toSecondsSinceMidnight(cameraRecordDuoji1.getStartTime())); + r.setEndTimeLong(toSecondsSinceMidnight(cameraRecordDuoji1.getEndTime())); + return r; + }) + .collect(Collectors.toList()); + record.setUrls(urls); + + return new Result<>(record); + } + + @PostMapping("/convetor") + @ApiOperation(value = "视频转换 ") + @LicenseHandler + public Result convetor(@RequestBody Record record) { + String url = record.getUrl().replace(".mp4","").replace("/"+configProperties.getS3Config().getBucketName() ,"").substring(1); + // 文件或目录的路径 + String filePath =configProperties.getS3Config().getUrlCache() +url+".mp4"; + // 创建一个File对象 + File file = new File(filePath); + if(file.exists()){ + return new Result<>(true); + }else { + s3Utils.downloadFile(configProperties.getS3Config().getBucketName(), url, configProperties.getS3Config().getUrlCache() + url); + HikCameraControlModuleImpl.convetor(configProperties.getS3Config().getUrlCache() + url, filePath); + return new Result<>(true); + } + } + private static long toSecondsSinceMidnight(LocalDateTime dateTime) { + LocalDateTime midnight = dateTime.toLocalDate().atStartOfDay(); + Duration duration = Duration.between(midnight, dateTime); + return duration.getSeconds(); + } +} diff --git a/web/src/main/java/com/leaper/web/entity/CameraRecordDuoji.java b/web/src/main/java/com/leaper/web/entity/CameraRecordDuoji.java new file mode 100644 index 0000000..11712e5 --- /dev/null +++ b/web/src/main/java/com/leaper/web/entity/CameraRecordDuoji.java @@ -0,0 +1,25 @@ +package com.leaper.web.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("camera_record_duoji") +public class CameraRecordDuoji { + @TableId(type = IdType.AUTO) + private Integer id; + private Integer cameraId; + private Integer type; + + @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; + private String path; + +} diff --git a/web/src/main/java/com/leaper/web/entity/Record.java b/web/src/main/java/com/leaper/web/entity/Record.java new file mode 100644 index 0000000..a66d16f --- /dev/null +++ b/web/src/main/java/com/leaper/web/entity/Record.java @@ -0,0 +1,17 @@ +package com.leaper.web.entity; + +import lombok.Data; + +import java.util.List; + +@Data +public class Record { + private String date; + private Integer cameraId; + private Long startTimeLong; + private Long endTimeLong; + private String url; + private List urls; + + +} diff --git a/web/src/main/java/com/leaper/web/lib/CameraControlModule.java b/web/src/main/java/com/leaper/web/lib/CameraControlModule.java index 15471b1..107cbcf 100644 --- a/web/src/main/java/com/leaper/web/lib/CameraControlModule.java +++ b/web/src/main/java/com/leaper/web/lib/CameraControlModule.java @@ -111,6 +111,7 @@ public interface CameraControlModule { void downloadMp4(Integer cameraId, String path, LocalDateTime start, LocalDateTime end, ConfigProperties.SavePath savePath); void downloadMp4(Integer cameraId, String path, LocalDateTime start, LocalDateTime end, ConfigProperties.SavePath savePath,Integer channel); + boolean downloadMp4Stream(Integer cameraId, String path, LocalDateTime start, LocalDateTime end,Integer channel); /** * 设置预置点 * diff --git a/web/src/main/java/com/leaper/web/lib/hik/HikCameraControlModuleImpl.java b/web/src/main/java/com/leaper/web/lib/hik/HikCameraControlModuleImpl.java index de43d11..dad711d 100644 --- a/web/src/main/java/com/leaper/web/lib/hik/HikCameraControlModuleImpl.java +++ b/web/src/main/java/com/leaper/web/lib/hik/HikCameraControlModuleImpl.java @@ -303,6 +303,50 @@ public class HikCameraControlModuleImpl implements CameraControlModule { } } + public boolean downloadMp4Stream(Integer cameraId, String path, LocalDateTime start, LocalDateTime end,Integer channel) { + + PathUtil.checkDirc(path); + HCNetSDK.NET_DVR_TIME startTime = new HCNetSDK.NET_DVR_TIME(); + startTime.setTime(start.getYear(), start.getMonthValue(), start.getDayOfMonth(), start.getHour(), start.getMinute(), start.getSecond()); + HCNetSDK.NET_DVR_TIME endTime = new HCNetSDK.NET_DVR_TIME(); + endTime.setTime(end.getYear(), end.getMonthValue(), end.getDayOfMonth(), end.getHour(), end.getMinute(), end.getSecond()); + + log.info("start download mp4 path:{} ,cameraId:{},start_time:{},end_time:{}",path,cameraId,startTime.toStringTime(),endTime.toStringTime()); + int lUserID = CameraConnMap.getConnId(cameraId).intValue(); + + int result = HikLoginModuleImpl.hcNetsdk.NET_DVR_GetFileByTime(lUserID, channel, startTime, endTime, path); + if (result == -1) { + log.error("downloadMp4 error code:{},cameraId:{},path:{}", HikLoginModuleImpl.hcNetsdk.NET_DVR_GetLastError(),cameraId,path); + return false; + } else { + HikLoginModuleImpl.hcNetsdk.NET_DVR_PlayBackControl(result, HikLoginModuleImpl.hcNetsdk.NET_DVR_PLAYSTART,0,null); + + + int tmp = -1; + IntByReference pos = new IntByReference(0); + while (result >= 0) { + boolean backFlag = HikLoginModuleImpl.hcNetsdk.NET_DVR_PlayBackControl(result, HikLoginModuleImpl.hcNetsdk.NET_DVR_PLAYGETPOS, 0, pos); + if (!backFlag) {//防止单个线程死循环 + return false; + } + int produce = pos.getValue(); + if ((produce % 10) == 0 && tmp != produce) {//输出进度 + tmp = produce; + } + if (produce == 100) {//下载成功 + HikLoginModuleImpl.hcNetsdk.NET_DVR_StopGetFile(result); + return true; + } + if (produce > 100) {//下载失败 + HikLoginModuleImpl.hcNetsdk.NET_DVR_StopGetFile(result); + result = -1; + return false; + } + } + return true; + } + } + class DownloadTask extends java.util.TimerTask { private Integer handler; private Timer downloadtimer; @@ -377,6 +421,7 @@ public class HikCameraControlModuleImpl implements CameraControlModule { while ((line = br.readLine()) != null) { } }catch (Exception e){ + }finally { try { if (br != null) { diff --git a/web/src/main/java/com/leaper/web/lib/hik/HikExceptionCallBack.java b/web/src/main/java/com/leaper/web/lib/hik/HikExceptionCallBack.java index 457e15d..f582b1d 100644 --- a/web/src/main/java/com/leaper/web/lib/hik/HikExceptionCallBack.java +++ b/web/src/main/java/com/leaper/web/lib/hik/HikExceptionCallBack.java @@ -16,7 +16,7 @@ public class HikExceptionCallBack implements HCNetSDK.FExceptionCallBack { if(cameraId != null){ log.error("hik disconnect,cameraId:{}", cameraId); - CameraConnMap.disConn(cameraId); + //CameraConnMap.disConn(cameraId); } }else if(dwType == 32791){ diff --git a/web/src/main/java/com/leaper/web/lib/joyware/JoywareCameraControlModuleImpl.java b/web/src/main/java/com/leaper/web/lib/joyware/JoywareCameraControlModuleImpl.java index 3747ebf..e881165 100644 --- a/web/src/main/java/com/leaper/web/lib/joyware/JoywareCameraControlModuleImpl.java +++ b/web/src/main/java/com/leaper/web/lib/joyware/JoywareCameraControlModuleImpl.java @@ -305,7 +305,9 @@ public class JoywareCameraControlModuleImpl implements CameraControlModule { log.error("download mp4 error:{},startTime:{} ,endTime:{},cameraId:{}",ToolKits.getErrorCodePrint(),start,end,cameraId); } } - + public boolean downloadMp4Stream(Integer cameraId, String path, LocalDateTime start, LocalDateTime end, Integer channel){ + return false; + } public void downloadMp4(Integer cameraId, String path, LocalDateTime start, LocalDateTime end, ConfigProperties.SavePath savePath,Integer channel) { diff --git a/web/src/main/java/com/leaper/web/mapper/CameraRecordDuojiMapper.java b/web/src/main/java/com/leaper/web/mapper/CameraRecordDuojiMapper.java new file mode 100644 index 0000000..3fe4d5d --- /dev/null +++ b/web/src/main/java/com/leaper/web/mapper/CameraRecordDuojiMapper.java @@ -0,0 +1,8 @@ +package com.leaper.web.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.leaper.web.entity.CameraRecord; +import com.leaper.web.entity.CameraRecordDuoji; + +public interface CameraRecordDuojiMapper extends BaseMapper { +} diff --git a/web/src/main/java/com/leaper/web/service/CameraService.java b/web/src/main/java/com/leaper/web/service/CameraService.java index d900120..22959ee 100644 --- a/web/src/main/java/com/leaper/web/service/CameraService.java +++ b/web/src/main/java/com/leaper/web/service/CameraService.java @@ -132,12 +132,12 @@ public class CameraService { if (ok) { camera.setStatus("连接正常"); } else { - CameraConnMap.disConn(camera.getId()); + //CameraConnMap.disConn(camera.getId()); camera.setStatus("未连接"); cameraLogin(camera); } }).collect(Collectors.toList()); - return new PageInfo<>(cameras); + return new PageInfo<>(collect); } public class StatusThread extends Thread { @@ -166,11 +166,11 @@ public class CameraService { if (ok) { camera.setStatus("连接正常"); } else { - CameraConnMap.disConn(camera.getId()); + //CameraConnMap.disConn(camera.getId()); camera.setStatus("未连接"); } } catch (Exception e) { - CameraConnMap.disConn(camera.getId()); + //CameraConnMap.disConn(camera.getId()); camera.setStatus("未连接"); } finally { latch.countDown(); diff --git a/web/src/main/java/com/leaper/web/service/CronTab.java b/web/src/main/java/com/leaper/web/service/CronTab.java index 046817c..50caa45 100644 --- a/web/src/main/java/com/leaper/web/service/CronTab.java +++ b/web/src/main/java/com/leaper/web/service/CronTab.java @@ -4,20 +4,16 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.SftpATTRS; import com.leaper.web.config.ConfigProperties; -import com.leaper.web.entity.Camera; -import com.leaper.web.entity.LightSource; -import com.leaper.web.entity.OrderLive; -import com.leaper.web.entity.PicData; +import com.leaper.web.entity.*; import com.leaper.web.lib.CameraConnMap; +import com.leaper.web.lib.CameraControlModule; import com.leaper.web.lib.hik.HikLoginModuleImpl; import com.leaper.web.lib.joyware.JoywareLoginModuleImpl; -import com.leaper.web.mapper.CameraMapper; -import com.leaper.web.mapper.LightSourceMapper; -import com.leaper.web.mapper.OrderLiveMapper; -import com.leaper.web.mapper.PicDataMapper; +import com.leaper.web.mapper.*; import com.leaper.web.service.damLightSource.JYDAMEquip; import com.leaper.web.service.damLightSource.JYDamHelper; import com.leaper.web.service.hikLightSource.HikControlSocket; +import com.leaper.web.task.S3Utils; import lombok.extern.slf4j.Slf4j; import net.schmizz.sshj.sftp.SFTPClient; import org.springframework.scheduling.annotation.EnableScheduling; @@ -32,10 +28,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.time.*; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjusters; +import java.util.*; import java.util.concurrent.CountDownLatch; @Component @@ -100,6 +95,87 @@ public class CronTab { } } + + /** + * 递归地清除指定目录下的所有文件和子目录。 + * + * @param directory 要清除的目录路径 + */ + @Scheduled(cron = "15 0 0 * * ?") + public void clearDirectoryRecursively() { + File directory = new File(configProperties.getS3Config().getUrlCache()); + if (directory.exists() && directory.isDirectory()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + // 递归调用此方法来清除子目录 + clearDirectoryRecursively(file); + } + // 删除文件或空目录 + file.delete(); + } + } + } + } + public static void clearDirectoryRecursively(File directory) { + if (directory.exists() && directory.isDirectory()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + // 递归调用此方法来清除子目录 + clearDirectoryRecursively(file); + } + // 删除文件或空目录 + file.delete(); + } + } + } + } + @Resource + S3Utils s3Utils; + @Resource + CameraRecordDuojiMapper cameraRecordDuojiMapper; + + @Resource + CameraControlModule cameraControlModule; + + /** + * 每分钟下载前两分钟的文件 + */ + @Scheduled(cron = "5 0/1 * * * ? ") + //@Scheduled(cron = "0 0/1 * * * *") + public void s3DownloadFile() { + List cameras = cameraMapper.selectList(new QueryWrapper<>()); + LocalDate currentDate = LocalDate.now(); // 获取当前日期 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 设置日期格式 + LocalDateTime now = LocalDateTime.now(); + //结束时间 + LocalDateTime currentMinuteStart = now.minusMinutes(2).withSecond(0).withNano(0); + //开始时间 + LocalDateTime previousMinuteStart = now.minusMinutes(3).withSecond(0).withNano(0); + String formattedDate = currentDate.format(formatter); + for (Camera camera : cameras){ + String url = formattedDate + "/" +UUID.randomUUID(); + boolean ok =cameraControlModule.downloadMp4Stream(camera.getId(), "D:\\" + url, previousMinuteStart, currentMinuteStart, camera.getChannel()); + if(ok) { + File file = new File("D:\\" + url); + s3Utils.uploadFile(configProperties.getS3Config().getBucketName(), url, file); + CameraRecordDuoji cameraRecordDuoji = new CameraRecordDuoji(); + cameraRecordDuoji.setCameraId(camera.getId()); + cameraRecordDuoji.setPath(configProperties.getS3Config().getBucketName()+ "/" +url); + cameraRecordDuoji.setStartTime(previousMinuteStart); + cameraRecordDuoji.setEndTime(currentMinuteStart); + cameraRecordDuoji.setPath(url); + cameraRecordDuojiMapper.insert(cameraRecordDuoji); + + file.delete(); + } + } + } + + @Scheduled(cron = "0 * * * * ?") public void cameraConn() { log.info("球机连接判断"); @@ -144,12 +220,12 @@ public class CronTab { if (ok) { camera.setStatus("连接正常"); } else { - CameraConnMap.disConn(camera.getId()); + //CameraConnMap.disConn(camera.getId()); camera.setStatus("未连接"); //cameraService.cameraLogin(camera); } } catch (Exception e) { - CameraConnMap.disConn(camera.getId()); + //CameraConnMap.disConn(camera.getId()); camera.setStatus("未连接"); } finally { latch.countDown(); diff --git a/web/src/main/java/com/leaper/web/task/S3Utils.java b/web/src/main/java/com/leaper/web/task/S3Utils.java index 1754ad4..4ced3bd 100644 --- a/web/src/main/java/com/leaper/web/task/S3Utils.java +++ b/web/src/main/java/com/leaper/web/task/S3Utils.java @@ -19,6 +19,7 @@ import com.amazonaws.services.s3.model.S3ObjectInputStream; import com.amazonaws.services.s3.transfer.internal.TransferManagerUtils; import com.amazonaws.util.IOUtils; import com.amazonaws.util.json.Jackson; +import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; diff --git a/web/src/main/java/com/leaper/web/task/T.java b/web/src/main/java/com/leaper/web/task/T.java deleted file mode 100644 index 514e89a..0000000 --- a/web/src/main/java/com/leaper/web/task/T.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.leaper.web.task; - -public class T { -} diff --git a/web/src/main/resources/application-prod.yml b/web/src/main/resources/application-prod.yml index db6b32f..91aaf05 100644 --- a/web/src/main/resources/application-prod.yml +++ b/web/src/main/resources/application-prod.yml @@ -108,4 +108,11 @@ deleteFileDays: 365 mybatis-plus: configuration: - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl \ No newline at end of file + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +s3Config: + accessKey: "minio" + secretKey: "minio123" + host: "192.168.1.52:9000" + bucketName: "camera" + urlCache: "D://camera/" \ No newline at end of file