diff --git a/cores/esp8266/FS.cpp b/cores/esp8266/FS.cpp index fa841c64f..392955d49 100644 --- a/cores/esp8266/FS.cpp +++ b/cores/esp8266/FS.cpp @@ -187,6 +187,13 @@ time_t File::getLastWrite() { return _p->getLastWrite(); } +time_t File::getCreationTime() { + if (!_p) + return 0; + + return _p->getCreationTime(); +} + void File::setTimeCallback(time_t (*cb)(void)) { if (!_p) return; @@ -224,6 +231,12 @@ time_t Dir::fileTime() { return _impl->fileTime(); } +time_t Dir::fileCreationTime() { + if (!_impl) + return 0; + return _impl->fileCreationTime(); +} + size_t Dir::fileSize() { if (!_impl) { return 0; @@ -262,17 +275,11 @@ bool Dir::rewind() { return _impl->rewind(); } -time_t Dir::getLastWrite() { - if (!_impl) - return 0; - - return _impl->getLastWrite(); -} - void Dir::setTimeCallback(time_t (*cb)(void)) { if (!_impl) return; _impl->setTimeCallback(cb); + timeCallback = cb; } @@ -289,6 +296,7 @@ bool FS::begin() { DEBUGV("#error: FS: no implementation"); return false; } + _impl->setTimeCallback(timeCallback); bool ret = _impl->begin(); DEBUGV("%s\n", ret? "": "#error: FS could not start"); return ret; diff --git a/cores/esp8266/FS.h b/cores/esp8266/FS.h index 23d05bad8..aa4752781 100644 --- a/cores/esp8266/FS.h +++ b/cores/esp8266/FS.h @@ -112,6 +112,7 @@ public: String readString() override; time_t getLastWrite(); + time_t getCreationTime(); void setTimeCallback(time_t (*cb)(void)); protected: @@ -120,7 +121,6 @@ protected: // Arduino SD class emulation std::shared_ptr _fakeDir; FS *_baseFS; - time_t (*timeCallback)(void) = nullptr; }; class Dir { @@ -132,20 +132,19 @@ public: String fileName(); size_t fileSize(); time_t fileTime(); + time_t fileCreationTime(); bool isFile() const; bool isDirectory() const; bool next(); bool rewind(); - time_t getLastWrite(); void setTimeCallback(time_t (*cb)(void)); protected: DirImplPtr _impl; FS *_baseFS; time_t (*timeCallback)(void) = nullptr; - }; // Backwards compatible, <4GB filesystem usage diff --git a/cores/esp8266/FSImpl.h b/cores/esp8266/FSImpl.h index 9715c65a8..1a3566f2c 100644 --- a/cores/esp8266/FSImpl.h +++ b/cores/esp8266/FSImpl.h @@ -51,6 +51,8 @@ public: // as the FS is allowed to return either the time of the last write() operation or the // time present in the filesystem metadata (often the last time the file was closed) virtual time_t getLastWrite() { return 0; } // Default is to not support timestamps + // Same for creation time. + virtual time_t getCreationTime() { return 0; } // Default is to not support timestamps protected: time_t (*timeCallback)(void) = nullptr; @@ -75,7 +77,11 @@ public: virtual FileImplPtr openFile(OpenMode openMode, AccessMode accessMode) = 0; virtual const char* fileName() = 0; virtual size_t fileSize() = 0; + // Return the last written time for a file. Undefined when called on a writable file + // as the FS is allowed to return either the time of the last write() operation or the + // time present in the filesystem metadata (often the last time the file was closed) virtual time_t fileTime() { return 0; } // By default, FS doesn't report file times + virtual time_t fileCreationTime() { return 0; } // By default, FS doesn't report file times virtual bool isFile() const = 0; virtual bool isDirectory() const = 0; virtual bool next() = 0; @@ -86,11 +92,6 @@ public: // same name. The default implementation simply returns time(&null) virtual void setTimeCallback(time_t (*cb)(void)) { timeCallback = cb; } - // Return the last written time for a file. Undefined when called on a writable file - // as the FS is allowed to return either the time of the last write() operation or the - // time present in the filesystem metadata (often the last time the file was closed) - virtual time_t getLastWrite() { return 0; } // Default is to not support timestamps - protected: time_t (*timeCallback)(void) = nullptr; }; diff --git a/doc/filesystem.rst b/doc/filesystem.rst index fecd84074..abf47a7ea 100644 --- a/doc/filesystem.rst +++ b/doc/filesystem.rst @@ -535,6 +535,11 @@ fileTime Returns the time_t write time of the current file pointed to by the internal iterator. +fileCreationTime +~~~~~~~~~~~~~~~~ +Returns the time_t creation time of the current file +pointed to by the internal iterator. + isFile ~~~~~~ @@ -642,6 +647,11 @@ getLastWrite Returns the file last write time, and only valid for files opened in read-only mode. If a file is opened for writing, the returned time may be indeterminate. +getCreationTime +~~~~~~~~~~~~~~~ + +Returns the file creation time, if available. + isFile ~~~~~~ diff --git a/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino b/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino index ce85de034..dfec9ee30 100644 --- a/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino +++ b/libraries/LittleFS/examples/LittleFS_Timestamp/LittleFS_Timestamp.ino @@ -53,9 +53,12 @@ void listDir(const char * dirname) { Serial.print(root.fileName()); Serial.print(" SIZE: "); Serial.print(file.size()); - time_t t = file.getLastWrite(); - struct tm * tmstruct = localtime(&t); + time_t cr = file.getCreationTime(); + time_t lw = file.getLastWrite(); file.close(); + struct tm * tmstruct = localtime(&cr); + Serial.printf(" CREATION: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); + tmstruct = localtime(&lw); Serial.printf(" LAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); } } @@ -90,6 +93,7 @@ void writeFile(const char * path, const char * message) { } else { Serial.println("Write failed"); } + delay(2000); // Make sure the CREATE and LASTWRITE times are different file.close(); } diff --git a/libraries/LittleFS/src/LittleFS.cpp b/libraries/LittleFS/src/LittleFS.cpp index 83041d222..b85075112 100644 --- a/libraries/LittleFS/src/LittleFS.cpp +++ b/libraries/LittleFS/src/LittleFS.cpp @@ -68,13 +68,26 @@ FileImplPtr LittleFSImpl::open(const char* path, OpenMode openMode, AccessMode a } free(pathStr); } + + time_t creation = 0; + if (timeCallback && (openMode & OM_CREATE)) { + // O_CREATE means we *may* make the file, but not if it already exists. + // See if it exists, and only if not update the creation time + int rc = lfs_file_open(&_lfs, fd.get(), path, LFS_O_RDONLY); + if (rc == 0) { + lfs_file_close(&_lfs, fd.get()); // It exists, don't update create time + } else { + creation = timeCallback(); // File didn't exist or otherwise, so we're going to create this time + } + } + int rc = lfs_file_open(&_lfs, fd.get(), path, flags); if (rc == LFS_ERR_ISDIR) { // To support the SD.openNextFile, a null FD indicates to the LittleFSFile this is just // a directory whose name we are carrying around but which cannot be read or written - return std::make_shared(this, path, nullptr, flags); + return std::make_shared(this, path, nullptr, flags, creation); } else if (rc == 0) { - return std::make_shared(this, path, fd, flags); + return std::make_shared(this, path, fd, flags, creation); } else { DEBUGV("LittleFSDirImpl::openFile: rc=%d fd=%p path=`%s` openMode=%d accessMode=%d err=%d\n", rc, fd.get(), path, openMode, accessMode, rc); diff --git a/libraries/LittleFS/src/LittleFS.h b/libraries/LittleFS/src/LittleFS.h index e332fd9ac..4d417905e 100644 --- a/libraries/LittleFS/src/LittleFS.h +++ b/libraries/LittleFS/src/LittleFS.h @@ -89,7 +89,7 @@ public: DirImplPtr openDir(const char *path) override; bool exists(const char* path) override { - if ( !_mounted || !path || !path[0] ) { + if (!_mounted || !path || !path[0]) { return false; } lfs_info info; @@ -98,7 +98,7 @@ public: } bool rename(const char* pathFrom, const char* pathTo) override { - if (!_mounted || !pathFrom || !pathFrom[0] || !pathTo || !pathTo[0]) { + if (!_mounted || !pathFrom || !pathFrom[0] || !pathTo || !pathTo[0]) { return false; } int rc = lfs_rename(&_lfs, pathFrom, pathTo); @@ -323,7 +323,7 @@ protected: class LittleFSFileImpl : public FileImpl { public: - LittleFSFileImpl(LittleFSImpl* fs, const char *name, std::shared_ptr fd, int flags) : _fs(fs), _fd(fd), _opened(true), _flags(flags) { + LittleFSFileImpl(LittleFSImpl* fs, const char *name, std::shared_ptr fd, int flags, time_t creation) : _fs(fs), _fd(fd), _opened(true), _flags(flags), _creation(creation) { _name = std::shared_ptr(new char[strlen(name) + 1], std::default_delete()); strcpy(_name.get(), name); } @@ -419,13 +419,20 @@ public: lfs_file_close(_fs->getFS(), _getFD()); _opened = false; DEBUGV("lfs_file_close: fd=%p\n", _getFD()); - if (timeCallback && (_flags & LFS_O_WRONLY)) { + if (timeCallback && (_flags & LFS_O_WRONLY)) { + // If the file opened with O_CREAT, write the creation time attribute + if (_creation) { + int rc = lfs_setattr(_fs->getFS(), _name.get(), 'c', (const void *)&_creation, sizeof(_creation)); + if (rc < 0) { + DEBUGV("Unable to set creation time on '%s' to %d\n", _name.get(), _creation); + } + } // Add metadata with last write time time_t now = timeCallback(); int rc = lfs_setattr(_fs->getFS(), _name.get(), 't', (const void *)&now, sizeof(now)); if (rc < 0) { - DEBUGV("Unable to set time on '%s' to %d\n", _name.get(), now); - } + DEBUGV("Unable to set last write time on '%s' to %d\n", _name.get(), now); + } } } } @@ -440,6 +447,16 @@ public: return ftime; } + time_t getCreationTime() override { + time_t ftime = 0; + if (_opened && _fd) { + int rc = lfs_getattr(_fs->getFS(), _name.get(), 'c', (void *)&ftime, sizeof(ftime)); + if (rc != sizeof(ftime)) + ftime = 0; // Error, so clear read value + } + return ftime; + } + const char* name() const override { if (!_opened) { return nullptr; @@ -484,6 +501,7 @@ protected: std::shared_ptr _name; bool _opened; int _flags; + time_t _creation; }; class LittleFSDirImpl : public DirImpl @@ -512,13 +530,9 @@ public: int nameLen = 3; // Slashes, terminator nameLen += _dirPath.get() ? strlen(_dirPath.get()) : 0; nameLen += strlen(_dirent.name); - char *tmpName = (char*)malloc(nameLen); - if (!tmpName) { - return FileImplPtr(); - } - snprintf(tmpName, nameLen, "%s%s%s", _dirPath.get() ? _dirPath.get() : "", _dirPath.get()&&_dirPath.get()[0]?"/":"", _dirent.name); + char tmpName[nameLen]; + snprintf(tmpName, nameLen, "%s%s%s", _dirPath.get() ? _dirPath.get() : "", _dirPath.get()&&_dirPath.get()[0]?"/":"", _dirent.name); auto ret = _fs->open((const char *)tmpName, openMode, accessMode); - free(tmpName); return ret; } @@ -537,23 +551,11 @@ public: } time_t fileTime() override { - if (!_valid) { - return 0; - } - int nameLen = 3; // Slashes, terminator - nameLen += _dirPath.get() ? strlen(_dirPath.get()) : 0; - nameLen += strlen(_dirent.name); - char *tmpName = (char*)malloc(nameLen); - if (!tmpName) { - return 0; - } - snprintf(tmpName, nameLen, "%s%s%s", _dirPath.get() ? _dirPath.get() : "", _dirPath.get()&&_dirPath.get()[0]?"/":"", _dirent.name); - time_t ftime = 0; - int rc = lfs_getattr(_fs->getFS(), tmpName, 't', (void *)&ftime, sizeof(ftime)); - if (rc != sizeof(ftime)) - ftime = 0; // Error, so clear read value - free(tmpName); - return ftime; + return (time_t)_getAttr4('t'); + } + + time_t fileCreationTime() override { + return (time_t)_getAttr4('c'); } @@ -592,6 +594,22 @@ protected: return _dir.get(); } + uint32_t _getAttr4(char attr) { + if (!_valid) { + return 0; + } + int nameLen = 3; // Slashes, terminator + nameLen += _dirPath.get() ? strlen(_dirPath.get()) : 0; + nameLen += strlen(_dirent.name); + char tmpName[nameLen]; + snprintf(tmpName, nameLen, "%s%s%s", _dirPath.get() ? _dirPath.get() : "", _dirPath.get()&&_dirPath.get()[0]?"/":"", _dirent.name); + time_t ftime = 0; + int rc = lfs_getattr(_fs->getFS(), tmpName, attr, (void *)&ftime, sizeof(ftime)); + if (rc != sizeof(ftime)) + ftime = 0; // Error, so clear read value + return ftime; + } + String _pattern; LittleFSImpl *_fs; std::shared_ptr _dir; diff --git a/libraries/SD/examples/listfiles/listfiles.ino b/libraries/SD/examples/listfiles/listfiles.ino index fd35d0c33..55478d708 100644 --- a/libraries/SD/examples/listfiles/listfiles.ino +++ b/libraries/SD/examples/listfiles/listfiles.ino @@ -29,9 +29,6 @@ File root; void setup() { // Open serial communications and wait for port to open: Serial.begin(115200); - while (!Serial) { - ; // wait for serial port to connect. Needed for Leonardo only - } Serial.print("Initializing SD card..."); @@ -71,11 +68,12 @@ void printDirectory(File dir, int numTabs) { // files have sizes, directories do not Serial.print("\t\t"); Serial.print(entry.size(), DEC); - Serial.print("\t\t"); - time_t ft = entry.getLastWrite(); - struct tm *tm = localtime(&ft); - // US format. Feel free to convert to your own locale... - Serial.printf("%02d-%02d-%02d %02d:%02d:%02d\n", tm->tm_mon + 1, tm->tm_mday, tm->tm_year % 100, tm->tm_hour, tm->tm_min, tm->tm_sec); + time_t cr = entry.getCreationTime(); + time_t lw = entry.getLastWrite(); + struct tm * tmstruct = localtime(&cr); + Serial.printf("\tCREATION: %d-%02d-%02d %02d:%02d:%02d", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); + tmstruct = localtime(&lw); + Serial.printf("\tLAST WRITE: %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct->tm_year) + 1900, (tmstruct->tm_mon) + 1, tmstruct->tm_mday, tmstruct->tm_hour, tmstruct->tm_min, tmstruct->tm_sec); } entry.close(); } diff --git a/libraries/SDFS/src/SDFS.h b/libraries/SDFS/src/SDFS.h index ee772cd5c..a884552ff 100644 --- a/libraries/SDFS/src/SDFS.h +++ b/libraries/SDFS/src/SDFS.h @@ -156,6 +156,7 @@ public: format(); _mounted = _fs.begin(_cfg._csPin, _cfg._spiSettings); } + sdfat::SdFile::dateTimeCallback(dateTimeCB); return _mounted; } @@ -204,6 +205,17 @@ public: return mktime(&tiempo); } + // Because SdFat has a single, global setting for this we can only use a + // static member of our class to return the time/date. However, since + // this is static, we can't see the time callback variable. Punt for now, + // using time(NULL) as the best we can do. + static void dateTimeCB(uint16_t *dosYear, uint16_t *dosTime) { + time_t now = time(nullptr); + struct tm *tiempo = localtime(&now); + *dosYear = ((tiempo->tm_year - 80) << 9) | ((tiempo->tm_mon + 1) << 5) | tiempo->tm_mday; + *dosTime = (tiempo->tm_hour << 11) | (tiempo->tm_min << 5) | tiempo->tm_sec; + } + protected: friend class SDFileImpl; friend class SDFSDirImpl; @@ -363,6 +375,18 @@ public: return ftime; } + time_t getCreationTime() override { + time_t ftime = 0; + if (_opened && _fd) { + sdfat::dir_t tmp; + if (_fd.get()->dirEntry(&tmp)) { + ftime = SDFSImpl::FatToTimeT(tmp.creationDate, tmp.creationTime); + } + } + return ftime; + } + + protected: SDFSImpl* _fs; @@ -426,6 +450,14 @@ public: return _time; } + time_t fileCreationTime() override + { + if (!_valid) { + return 0; + } + + return _creation; + } bool isFile() const override { @@ -451,8 +483,10 @@ public: sdfat::dir_t tmp; if (file.dirEntry(&tmp)) { _time = SDFSImpl::FatToTimeT(tmp.lastWriteDate, tmp.lastWriteTime); + _creation = SDFSImpl::FatToTimeT(tmp.creationDate, tmp.creationTime); } else { _time = 0; + _creation = 0; } file.getName(_lfn, sizeof(_lfn)); file.close(); @@ -477,6 +511,7 @@ protected: bool _valid; char _lfn[64]; time_t _time; + time_t _creation; std::shared_ptr _dirPath; uint32_t _size; bool _isFile; diff --git a/package/package_esp8266com_index.template.json b/package/package_esp8266com_index.template.json index 847eb4a12..c541cd192 100644 --- a/package/package_esp8266com_index.template.json +++ b/package/package_esp8266com_index.template.json @@ -130,7 +130,7 @@ }, { "packager": "esp8266", - "version": "2.5.0-4-69bd9e6", + "version": "2.5.0-4-fe5bb56", "name": "mklittlefs" }, { @@ -311,22 +311,29 @@ ] }, { - "version": "2.5.0-4-69bd9e6", + "version": "2.5.0-4-fe5bb56", "name": "mklittlefs", "systems": [ { "host": "aarch64-linux-gnu", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/aarch64-linux-gnu-mklittlefs-69bd9e6.tar.gz", - "archiveFileName": "aarch64-linux-gnu-mklittlefs-69bd9e6.tar.gz", - "checksum": "SHA-256:74d938f15a3fb8ac20aeb0f938ace2c6759f622451419c09446aa79866302e18", - "size": "44342" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/aarch64-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "archiveFileName": "aarch64-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "checksum": "SHA-256:ac50bae3b580053ba98a181ae3700fafd2b2f8a37ed9c16bc22a5d7c1659388e", + "size": "44433" }, { "host": "arm-linux-gnueabihf", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/arm-linux-gnueabihf-mklittlefs-69bd9e6.tar.gz", - "archiveFileName": "arm-linux-gnueabihf-mklittlefs-69bd9e6.tar.gz", - "checksum": "SHA-256:926cca1c1f8f732a8ac79809ce0a52cabe283ab4137aa3237bca0fcca6bc2236", - "size": "36871" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/arm-linux-gnueabihf.mklittlefs-fe5bb56.1578453304.tar.gz", + "archiveFileName": "arm-linux-gnueabihf.mklittlefs-fe5bb56.1578453304.tar.gz", + "checksum": "SHA-256:092555612e7e229fbe622df75db70560896c3aea8d0ac2e5fa16d92dc16857cf", + "size": "36917" + }, + { + "host": "i686-pc-linux-gnu", + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/i686-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "archiveFileName": "i686-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "checksum": "SHA-256:060e2525223269d2a5d01055542ff36837f0b19598d78cb02d58563aeda441cd", + "size": "47833" }, { "host": "i686-pc-linux-gnu", @@ -337,35 +344,35 @@ }, { "host": "i686-mingw32", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/i686-w64-mingw32-mklittlefs-69bd9e6.zip", - "archiveFileName": "i686-w64-mingw32-mklittlefs-69bd9e6.zip", - "checksum": "SHA-256:da916c66f70e162f4aec22dbcb4542dd8b8187d12c35c915d563e2262cfe6fbd", - "size": "332325" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/i686-w64-mingw32.mklittlefs-fe5bb56.1578453304.zip", + "archiveFileName": "i686-w64-mingw32.mklittlefs-fe5bb56.1578453304.zip", + "checksum": "SHA-256:2e570bed4ec59a9ecc73290e16c31ed53ee15e3abd8c82cb038b2148596d112e", + "size": "332329" }, { "host": "x86_64-apple-darwin", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-apple-darwin14-mklittlefs-69bd9e6.tar.gz", - "archiveFileName": "x86_64-apple-darwin14-mklittlefs-69bd9e6.tar.gz", - "checksum": "SHA-256:35610be5f725121eaa9baea83c686693f340742e61739af6789d00feff4e90ba", - "size": "362366" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-apple-darwin14.mklittlefs-fe5bb56.1578453304.tar.gz", + "archiveFileName": "x86_64-apple-darwin14.mklittlefs-fe5bb56.1578453304.tar.gz", + "checksum": "SHA-256:fcb57ff58eceac79e988cc26a9e009a11ebda68d4ae97e44fed8e7c6d98a35b5", + "size": "362389" }, { "host": "x86_64-pc-linux-gnu", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-linux-gnu-mklittlefs-69bd9e6.tar.gz", - "archiveFileName": "x86_64-linux-gnu-mklittlefs-69bd9e6.tar.gz", - "checksum": "SHA-256:e4ce7cc80eceab6a9a2e620f2badfb1ef09ee88f7af529f290c65b4b72f19358", - "size": "46518" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "archiveFileName": "x86_64-linux-gnu.mklittlefs-fe5bb56.1578453304.tar.gz", + "checksum": "SHA-256:5ef79d76e8e76f8287dc70d10c33f020d4cf5320354571adf666665eeef2e2de", + "size": "46580" }, { "host": "x86_64-mingw32", - "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-w64-mingw32-mklittlefs-69bd9e6.zip", - "archiveFileName": "x86_64-w64-mingw32-mklittlefs-69bd9e6.zip", - "checksum": "SHA-256:c65ee1ee38f65ce67f664bb3118301ee6e93bec38a7a7efaf8e1d8455c6a4a18", - "size": "344780" + "url": "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-4/x86_64-w64-mingw32.mklittlefs-fe5bb56.1578453304.zip", + "archiveFileName": "x86_64-w64-mingw32.mklittlefs-fe5bb56.1578453304.zip", + "checksum": "SHA-256:a460f410a22a59e23d7f862b8d08d6b7dfbc93aa558f8161a3d640d4df2ab86f", + "size": "344792" } ] } ] } ] -} \ No newline at end of file +}