From 94530bda30b847d264beaeb7ed2a2b819531c4de Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 11 Sep 2017 16:17:59 -0400 Subject: [PATCH] http-service: add cache service --- http-service/pom.xml | 15 + .../service/SpringBootWebApplication.java | 8 + .../runelite/http/service/cache/CacheDAO.java | 260 ++++++++++++ .../http/service/cache/CacheSecurity.java | 59 +++ .../http/service/cache/CacheService.java | 387 ++++++++++++++++++ .../http/service/cache/CacheStorage.java | 126 ++++++ .../http/service/cache/CacheUpdater.java | 159 +++++++ .../http/service/cache/CacheUploader.java | 90 ++++ .../http/service/cache/NotFoundException.java | 34 ++ .../service/cache/OutOfDateException.java | 33 ++ .../service/cache/beans/ArchiveEntry.java | 126 ++++++ .../http/service/cache/beans/CacheEntry.java | 112 +++++ .../http/service/cache/beans/FileEntry.java | 126 ++++++ .../http/service/cache/beans/IndexEntry.java | 109 +++++ .../runelite/http/service/cache/schema.sql | 132 ++++++ .../service/SpringBootWebApplicationTest.java | 10 +- .../src/test/resources/application.properties | 7 +- 17 files changed, 1791 insertions(+), 2 deletions(-) create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheSecurity.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheService.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheStorage.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheUpdater.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/CacheUploader.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/NotFoundException.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/OutOfDateException.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java create mode 100644 http-service/src/main/java/net/runelite/http/service/cache/schema.sql diff --git a/http-service/pom.xml b/http-service/pom.xml index 381556c50f..18dfc67df4 100644 --- a/http-service/pom.xml +++ b/http-service/pom.xml @@ -51,6 +51,11 @@ ${spring.boot.version} provided + + org.springframework.boot + spring-boot-starter-security + ${spring.boot.version} + org.springframework.boot spring-boot-devtools @@ -63,6 +68,11 @@ http-api ${project.version} + + net.runelite + cache + ${project.version} + org.sql2o @@ -89,6 +99,11 @@ scribejava-apis 4.1.0 + + io.minio + minio + 3.0.6 + org.springframework.boot diff --git a/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java b/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java index be455fc7f3..5f259427dd 100644 --- a/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java +++ b/http-service/src/main/java/net/runelite/http/service/SpringBootWebApplication.java @@ -56,7 +56,15 @@ public class SpringBootWebApplication extends SpringBootServletInitializer Map converters = new HashMap<>(); converters.put(Instant.class, new InstantConverter()); return new Sql2o(dataSource, new NoQuirks(converters)); + } + @Bean("Runelite Cache SQL2O") + Sql2o cacheSql2o() throws NamingException + { + DataSource dataSource = (DataSource) getContext().lookup("jdbc/runelite-cache"); + Map converters = new HashMap<>(); + converters.put(Instant.class, new InstantConverter()); + return new Sql2o(dataSource, new NoQuirks(converters)); } @Override diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java new file mode 100644 index 0000000000..60241bc4ac --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheDAO.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import java.time.Instant; +import java.util.List; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.FileEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.sql2o.Connection; +import org.sql2o.Query; + +public class CacheDAO +{ + // cache prepared statements for high volume queries + private Query associateArchive; + private Query findArchive, insertArchive; + private Query associateFile; + + public List listCaches(Connection con) + { + return con.createQuery("select id, revision, date from cache") + .executeAndFetch(CacheEntry.class); + } + + public CacheEntry findMostRecent(Connection con) + { + return con.createQuery("select id from cache order by revision,date desc limit 1") + .executeAndFetchFirst(CacheEntry.class); + } + + public List findIndexesForCache(Connection con, CacheEntry cache) + { + return con.createQuery("select index.id, index.indexId, index.revision from cache " + + "join cache_index on cache_index.cache = cache.id " + + "join `index` on cache_index.index = index.id " + + "where cache.id = :id " + + "order by index.indexId asc") + .addParameter("id", cache.getId()) + .executeAndFetch(IndexEntry.class); + } + + public IndexEntry findIndexForCache(Connection con, CacheEntry cache, int indexId) + { + return con.createQuery("select index.id, index.indexId, index.revision from cache " + + "join cache_index on cache_index.cache = cache.id " + + "join `index` on cache_index.index = index.id " + + "where cache.id = :id " + + "and index.indexId = :indexId") + .addParameter("id", cache.getId()) + .addParameter("indexId", indexId) + .executeAndFetchFirst(IndexEntry.class); + } + + public List findArchivesForIndex(Connection con, IndexEntry indexEntry) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash, archive.revision from index_archive " + + "join archive on index_archive.archive = archive.id " + + "where index_archive.index = :id") + .addParameter("id", indexEntry.getId()) + .executeAndFetch(ArchiveEntry.class); + } + + public ArchiveEntry findArchiveForIndex(Connection con, IndexEntry indexEntry, int archiveId) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash, archive.revision from index_archive " + + "join archive on index_archive.archive = archive.id " + + "where index_archive.index = :id " + + "and archive.archiveId = :archiveId") + .addParameter("id", indexEntry.getId()) + .addParameter("archiveId", archiveId) + .executeAndFetchFirst(ArchiveEntry.class); + } + + /** + * Finds the most recent archive for the given indexId/archiveId + * @param con + * @param indexId + * @param archiveId + * @return + */ + public ArchiveEntry findMostRecentArchive(Connection con, int indexId, int archiveId) + { + return con.createQuery("select archive.id, archive.archiveId, archive.nameHash, archive.revision from archive " + + "join index_archive on index_archive.archive = archive.id " + + "join `index` on index.id = index_archive.index " + + "where index.indexId = :indexId and archive.archiveId = :archiveId " + + "group by archive.id " + + "order by archive.revision desc " + + "limit 1") + .addParameter("indexId", indexId) + .addParameter("archiveId", archiveId) + .executeAndFetchFirst(ArchiveEntry.class); + } + + public List findFilesForArchive(Connection con, ArchiveEntry archiveEntry) + { + return con.createQuery("select id, fileId, nameHash from file " + + "where archive = :archive") + .addParameter("archive", archiveEntry.getId()) + .executeAndFetch(FileEntry.class); + } + + public CacheEntry createCache(Connection con, int revision, Instant date) + { + int cacheId = con.createQuery("insert into cache (revision, date) values (:revision, :date)") + .addParameter("revision", revision) + .addParameter("date", date) + .executeUpdate() + .getKey(int.class); + + CacheEntry entry = new CacheEntry(); + entry.setId(cacheId); + entry.setRevision(revision); + entry.setDate(date); + return entry; + } + + public CacheEntry findCache(Connection con, int cacheId) + { + return con.createQuery("select id, revision, date from cache " + + "where id = :cacheId") + .addParameter("cacheId", cacheId) + .executeAndFetchFirst(CacheEntry.class); + } + + public IndexEntry findIndex(Connection con, int indexId, int revision) + { + return con.createQuery("select id, indexId, revision from `index` " + + "where indexId = :indexId " + + "and revision = :revision") + .addParameter("indexId", indexId) + .addParameter("revision", revision) + .executeAndFetchFirst(IndexEntry.class); + } + + public void associateIndexToCache(Connection con, CacheEntry cache, IndexEntry index) + { + con.createQuery("insert into cache_index (cache, `index`) values (:cache, :index)") + .addParameter("cache", cache.getId()) + .addParameter("index", index.getId()) + .executeUpdate(); + } + + public IndexEntry findOrCreateIndex(Connection con, CacheEntry cache, int indexId, int revision) + { + IndexEntry entry = con.createQuery("select id, indexId, revision from `index` where indexId = :indexId and revision = :revision") + .addParameter("indexId", indexId) + .addParameter("revision", revision) + .executeAndFetchFirst(IndexEntry.class); + + if (entry != null) + { + return entry; + } + + int id = con.createQuery("insert into `index` (indexId, revision) values (:indexId, :revision)") + .addParameter("indexId", indexId) + .addParameter("revision", revision) + .executeUpdate() + .getKey(int.class); + + entry = new IndexEntry(); + entry.setId(id); + entry.setIndexId(indexId); + entry.setRevision(revision); + return entry; + } + + public void associateArchiveToIndex(Connection con, ArchiveEntry archive, IndexEntry index) + { + if (associateArchive == null) + { + associateArchive = con.createQuery("insert into index_archive (`index`, archive) values (:index, :archive)"); + } + associateArchive + .addParameter("index", index.getId()) + .addParameter("archive", archive.getId()) + .executeUpdate(); + } + + public ArchiveEntry findArchive(Connection con, IndexEntry index, + int archiveId, int nameHash, int revision) + { + if (findArchive == null) + { + findArchive = con.createQuery("select archive.id, archive.archiveId, archive.nameHash, archive.revision from archive " + + " join index_archive on index_archive.archive = archive.id" + + " join `index` on index.id = index_archive.index" + + " where archive.archiveId = :archiveId and archive.revision = :revision and index.indexId = :indexId"); + } + + ArchiveEntry entry = findArchive + .addParameter("archiveId", archiveId) + .addParameter("revision", revision) + .addParameter("indexId", index.getIndexId()) + .executeAndFetchFirst(ArchiveEntry.class); + return entry; + } + + public ArchiveEntry createArchive(Connection con, IndexEntry index, + int archiveId, int nameHash, int revision) + { + if (insertArchive == null) + { + insertArchive = con.createQuery("insert into archive (archiveId, nameHash, revision) values " + + "(:archiveId, :nameHash, :revision)"); + } + + int id = insertArchive + .addParameter("archiveId", archiveId) + .addParameter("nameHash", nameHash) + .addParameter("revision", revision) + .executeUpdate() + .getKey(int.class); + + ArchiveEntry entry = new ArchiveEntry(); + entry.setId(id); + entry.setArchiveId(archiveId); + entry.setNameHash(nameHash); + entry.setRevision(revision); + return entry; + } + + public void associateFileToArchive(Connection con, ArchiveEntry archive, int fileId, int nameHash) + { + if (associateFile == null) + { + associateFile = con.createQuery("insert into file (archive, fileId, nameHash) values (:archive, :fileId, :nameHash)"); + } + associateFile + .addParameter("archive", archive.getId()) + .addParameter("fileId", fileId) + .addParameter("nameHash", nameHash) + .executeUpdate(); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheSecurity.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheSecurity.java new file mode 100644 index 0000000000..16a6794e56 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheSecurity.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class CacheSecurity extends WebSecurityConfigurerAdapter +{ + @Value("${auth.password}") + private String password; + + @Override + protected void configure(HttpSecurity http) throws Exception + { + http.httpBasic() + .and() + .authorizeRequests() + .antMatchers("/cache/admin/**") + .hasRole("ADMIN"); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception + { + auth.inMemoryAuthentication() + .withUser("admin") + .password(password) + .roles("ADMIN"); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java new file mode 100644 index 0000000000..42ef759231 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheService.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import com.google.common.io.ByteStreams; +import io.minio.MinioClient; +import io.minio.errors.ErrorResponseException; +import io.minio.errors.InsufficientDataException; +import io.minio.errors.InternalException; +import io.minio.errors.InvalidArgumentException; +import io.minio.errors.InvalidBucketNameException; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import io.minio.errors.NoResponseException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.stream.Collectors; +import net.runelite.cache.ConfigType; +import net.runelite.cache.IndexType; +import net.runelite.cache.definitions.ItemDefinition; +import net.runelite.cache.definitions.NpcDefinition; +import net.runelite.cache.definitions.ObjectDefinition; +import net.runelite.cache.definitions.loaders.ItemLoader; +import net.runelite.cache.definitions.loaders.NpcLoader; +import net.runelite.cache.definitions.loaders.ObjectLoader; +import net.runelite.cache.fs.ArchiveFiles; +import net.runelite.cache.fs.FSFile; +import net.runelite.cache.fs.jagex.DataFile; +import net.runelite.cache.fs.jagex.DataFileReadResult; +import net.runelite.http.api.cache.Cache; +import net.runelite.http.api.cache.CacheArchive; +import net.runelite.http.api.cache.CacheIndex; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.FileEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.sql2o.Connection; +import org.sql2o.Sql2o; +import org.xmlpull.v1.XmlPullParserException; + +@RestController +@RequestMapping("/cache") +public class CacheService +{ + private static final Logger logger = LoggerFactory.getLogger(CacheService.class); + + @Autowired + @Qualifier("Runelite Cache SQL2O") + private Sql2o sql2o; + + @Value("${minio.bucket}") + private String minioBucket; + + private final MinioClient minioClient; + + @Autowired + public CacheService( + @Value("${minio.endpoint}") String minioEndpoint, + @Value("${minio.accesskey}") String accessKey, + @Value("${minio.secretkey}") String secretKey + ) throws InvalidEndpointException, InvalidPortException + { + this.minioClient = new MinioClient(minioEndpoint, accessKey, secretKey); + } + + @Bean + public MinioClient minioClient() + { + return minioClient; + } + + /** + * retrieve archive from storage + * + * @param indexId + * @param archiveId + * @param revision + * @return + */ + private byte[] getArchive(int indexId, int archiveId, int revision) + { + String path = new StringBuilder() + .append(indexId) + .append('/') + .append(archiveId) + .append('/') + .append(revision) + .toString(); + + try (InputStream in = minioClient.getObject(minioBucket, path)) + { + return ByteStreams.toByteArray(in); + } + catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException | IOException | InvalidKeyException | NoResponseException | XmlPullParserException | ErrorResponseException | InternalException | InvalidArgumentException ex) + { + logger.warn(null, ex); + return null; + } + } + + private ArchiveFiles getArchiveFiles(IndexType index, ConfigType config, + ArchiveEntry archiveEntry) throws IOException + { + List files; + + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + files = cacheDao.findFilesForArchive(con, archiveEntry); + } + + byte[] archiveData = getArchive(index.getNumber(), config.getId(), archiveEntry.getRevision()); + + if (archiveData == null) + { + return null; + } + + DataFileReadResult result = DataFile.decompress(archiveData, null); + if (result == null) + { + return null; + } + + byte[] decompressedData = result.data; + + ArchiveFiles archiveFiles = new ArchiveFiles(); + for (FileEntry fileEntry : files) + { + FSFile file = new FSFile(fileEntry.getFileId()); + archiveFiles.addFile(file); + file.setNameHash(fileEntry.getNameHash()); + } + archiveFiles.loadContents(decompressedData); + return archiveFiles; + } + + @RequestMapping("/") + public List listCaches() + { + List caches; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + caches = cacheDao.listCaches(con); + } + return caches.stream() + .map(entry -> new Cache(entry.getId(), entry.getRevision(), entry.getDate())) + .collect(Collectors.toList()); + } + + @RequestMapping("{cacheId}") + public List listIndexes(@PathVariable int cacheId) + { + List indexes; + + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + CacheEntry cacheEntry = cacheDao.findCache(con, cacheId); + if (cacheEntry == null) + { + throw new NotFoundException(); + } + + indexes = cacheDao.findIndexesForCache(con, cacheEntry); + } + + return indexes.stream() + .map(entry -> new CacheIndex(entry.getIndexId(), entry.getRevision())) + .collect(Collectors.toList()); + } + + @RequestMapping("{cacheId}/{indexId}") + public List listArchives(@PathVariable int cacheId, + @PathVariable int indexId) + { + List archives; + + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + CacheEntry cacheEntry = cacheDao.findCache(con, cacheId); + if (cacheEntry == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheDao.findIndexForCache(con, cacheEntry, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + archives = cacheDao.findArchivesForIndex(con, indexEntry); + } + + return archives.stream() + .map(archive -> new CacheArchive(archive.getArchiveId(), archive.getNameHash(), archive.getRevision())) + .collect(Collectors.toList()); + } + + @RequestMapping("{cacheId}/{indexId}/{archiveId}") + public CacheArchive getCacheArchive(@PathVariable int cacheId, + @PathVariable int indexId, + @PathVariable int archiveId) + { + ArchiveEntry archiveEntry; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + CacheEntry cacheEntry = cacheDao.findCache(con, cacheId); + if (cacheEntry == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheDao.findIndexForCache(con, cacheEntry, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + archiveEntry = cacheDao.findArchiveForIndex(con, indexEntry, archiveId); + } + + return new CacheArchive(archiveEntry.getArchiveId(), + archiveEntry.getNameHash(), archiveEntry.getRevision()); + } + + @RequestMapping("{cacheId}/{indexId}/{archiveId}/data") + public byte[] getArchiveData( + @PathVariable int cacheId, + @PathVariable int indexId, + @PathVariable int archiveId + ) + { + ArchiveEntry archiveEntry; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + CacheEntry cacheEntry = cacheDao.findCache(con, cacheId); + if (cacheEntry == null) + { + throw new NotFoundException(); + } + + IndexEntry indexEntry = cacheDao.findIndexForCache(con, cacheEntry, indexId); + if (indexEntry == null) + { + throw new NotFoundException(); + } + + archiveEntry = cacheDao.findArchiveForIndex(con, indexEntry, archiveId); + } + + return getArchive(indexId, archiveId, archiveEntry.getRevision()); + } + + @RequestMapping("item/{itemId}") + public ItemDefinition getItem(@PathVariable int itemId) throws IOException + { + ArchiveEntry archiveEntry; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + archiveEntry = cacheDao.findMostRecentArchive(con, IndexType.CONFIGS.getNumber(), ConfigType.ITEM.getId()); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + } + + ArchiveFiles archiveFiles = getArchiveFiles(IndexType.CONFIGS, ConfigType.ITEM, archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(itemId); + if (file == null) + { + throw new NotFoundException(); + } + + ItemDefinition itemdef = new ItemLoader().load(itemId, file.getContents()); + return itemdef; + } + + @RequestMapping("object/{objectId}") + public ObjectDefinition getObject( + @PathVariable int objectId + ) throws IOException + { + ArchiveEntry archiveEntry; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + archiveEntry = cacheDao.findMostRecentArchive(con, IndexType.CONFIGS.getNumber(), ConfigType.OBJECT.getId()); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + } + + ArchiveFiles archiveFiles = getArchiveFiles(IndexType.CONFIGS, ConfigType.OBJECT, archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(objectId); + if (file == null) + { + throw new NotFoundException(); + } + + ObjectDefinition objectdef = new ObjectLoader().load(objectId, file.getContents()); + return objectdef; + } + + @RequestMapping("npc/{npcId}") + public NpcDefinition getNpc( + @PathVariable int npcId + ) throws IOException + { + ArchiveEntry archiveEntry; + try (Connection con = sql2o.open()) + { + CacheDAO cacheDao = new CacheDAO(); + archiveEntry = cacheDao.findMostRecentArchive(con, IndexType.CONFIGS.getNumber(), ConfigType.NPC.getId()); + if (archiveEntry == null) + { + throw new NotFoundException(); + } + } + + ArchiveFiles archiveFiles = getArchiveFiles(IndexType.CONFIGS, ConfigType.NPC, archiveEntry); + if (archiveFiles == null) + { + throw new NotFoundException(); + } + + FSFile file = archiveFiles.findFile(npcId); + if (file == null) + { + throw new NotFoundException(); + } + + NpcDefinition npcdef = new NpcLoader().load(npcId, file.getContents()); + return npcdef; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheStorage.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheStorage.java new file mode 100644 index 0000000000..37c0228233 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheStorage.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import java.io.IOException; +import java.util.List; +import net.runelite.cache.fs.Archive; +import net.runelite.cache.fs.FSFile; +import net.runelite.cache.fs.Index; +import net.runelite.cache.fs.Storage; +import net.runelite.cache.fs.Store; +import net.runelite.http.service.cache.beans.ArchiveEntry; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.FileEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.sql2o.Connection; + +public class CacheStorage implements Storage +{ + private CacheEntry cacheEntry; + private final CacheDAO cacheDao; + private final Connection con; + + public CacheStorage(CacheEntry cacheEntry, CacheDAO cacheDao, Connection con) + { + this.cacheEntry = cacheEntry; + this.cacheDao = cacheDao; + this.con = con; + } + + public CacheEntry getCacheEntry() + { + return cacheEntry; + } + + public void setCacheEntry(CacheEntry cacheEntry) + { + this.cacheEntry = cacheEntry; + } + + @Override + public void init(Store store) throws IOException + { + } + + @Override + public void close() throws IOException + { + } + + @Override + public void load(Store store) throws IOException + { + List indexes = cacheDao.findIndexesForCache(con, cacheEntry); + for (IndexEntry indexEntry : indexes) + { + Index index = store.addIndex(indexEntry.getIndexId()); + index.setRevision(indexEntry.getRevision()); + + List archives = cacheDao.findArchivesForIndex(con, indexEntry); + for (ArchiveEntry archiveEntry : archives) + { + Archive archive = index.addArchive(archiveEntry.getArchiveId()); + archive.setRevision(archiveEntry.getRevision()); + archive.setNameHash(archiveEntry.getNameHash()); + + List files = cacheDao.findFilesForArchive(con, archiveEntry); + for (FileEntry fileEntry : files) + { + FSFile file = archive.addFile(fileEntry.getFileId()); + file.setNameHash(fileEntry.getNameHash()); + } + } + } + } + + @Override + public void save(Store store) throws IOException + { + for (Index index : store.getIndexes()) + { + IndexEntry entry = cacheDao.findOrCreateIndex(con, cacheEntry, index.getId(), index.getRevision()); + // this assumes nothing is associated to the cache yet + cacheDao.associateIndexToCache(con, cacheEntry, entry); + + for (Archive archive : index.getArchives()) + { + ArchiveEntry archiveEntry = cacheDao.findArchive(con, entry, archive.getArchiveId(), + archive.getNameHash(), archive.getRevision()); + if (archiveEntry == null) + { + archiveEntry = cacheDao.createArchive(con, entry, archive.getArchiveId(), archive.getNameHash(), archive.getRevision()); + + for (FSFile file : archive.getFiles()) + { + cacheDao.associateFileToArchive(con, archiveEntry, file.getFileId(), file.getNameHash()); + } + } + cacheDao.associateArchiveToIndex(con, archiveEntry, entry); + } + } + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheUpdater.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheUpdater.java new file mode 100644 index 0000000000..5b2e936fac --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheUpdater.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import io.minio.MinioClient; +import io.minio.errors.InvalidEndpointException; +import io.minio.errors.InvalidPortException; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import net.runelite.cache.client.CacheClient; +import net.runelite.cache.client.IndexInfo; +import static net.runelite.cache.client.requests.HelloHandshake.RESPONSE_OK; +import net.runelite.cache.fs.Archive; +import net.runelite.cache.fs.Store; +import net.runelite.http.api.RuneliteAPI; +import net.runelite.http.service.cache.beans.CacheEntry; +import net.runelite.http.service.cache.beans.IndexEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.sql2o.Connection; +import org.sql2o.Sql2o; + +@RestController +@RequestMapping("/cache/admin") +public class CacheUpdater +{ + private static final Logger logger = LoggerFactory.getLogger(CacheUpdater.class); + + private final Sql2o sql2o; + private final MinioClient minioClient; + + @Value("${minio.bucket}") + private String minioBucket; + + @Autowired + public CacheUpdater( + @Qualifier("Runelite Cache SQL2O") Sql2o sql2o, + MinioClient minioClient + ) + { + this.sql2o = sql2o; + this.minioClient = minioClient; + } + + @RequestMapping("/update") + public void check() throws IOException, InvalidEndpointException, InvalidPortException, InterruptedException + { + int rsVersion = RuneliteAPI.getRsVersion(); + + try (Connection con = sql2o.beginTransaction()) + { + CacheDAO cacheDao = new CacheDAO(); + CacheEntry cache = cacheDao.findMostRecent(con); + boolean created = false; + if (cache == null) + { + created = true; + cache = cacheDao.createCache(con, rsVersion, Instant.now()); + } + + CacheStorage storage = new CacheStorage(cache, cacheDao, con); + Store store = new Store(storage); + store.load(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + CacheClient client = new CacheClient(store, rsVersion, + (Archive archive) -> executor.submit(new CacheUploader(minioClient, minioBucket, archive))); + + client.connect(); + int result = client.handshake().join(); + + if (result != RESPONSE_OK) + { + throw new OutOfDateException(); + } + + List indexes = client.requestIndexes(); + List entries = cacheDao.findIndexesForCache(con, cache); + + if (!checkOutOfDate(indexes, entries)) + { + return; + } + + client.download(); + + CacheEntry newCache = created ? cache : cacheDao.createCache(con, rsVersion, Instant.now()); + + storage.setCacheEntry(newCache); + store.save(); + + // ensure objects are added to the store before they become + // visible in the database + executor.shutdown(); + while (!executor.awaitTermination(1, TimeUnit.SECONDS)) + { + logger.debug("Waiting for termination of executor..."); + } + + // commit database + con.commit(); + } + } + + private boolean checkOutOfDate(List indexes, List dbIndexes) + { + if (indexes.size() != dbIndexes.size()) + { + return true; + } + + for (int i = 0; i < indexes.size(); ++i) + { + IndexInfo ii = indexes.get(i); + IndexEntry ie = dbIndexes.get(i); + + if (ii.getId() != ie.getIndexId() + || ii.getRevision() != ie.getRevision()) + { + return true; + } + } + + return false; + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/CacheUploader.java b/http-service/src/main/java/net/runelite/http/service/cache/CacheUploader.java new file mode 100644 index 0000000000..4ede3c4cf8 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/CacheUploader.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import io.minio.MinioClient; +import io.minio.errors.ErrorResponseException; +import io.minio.errors.InsufficientDataException; +import io.minio.errors.InternalException; +import io.minio.errors.InvalidArgumentException; +import io.minio.errors.InvalidBucketNameException; +import io.minio.errors.NoResponseException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import net.runelite.cache.fs.Archive; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmlpull.v1.XmlPullParserException; + +public class CacheUploader implements Runnable +{ + private static final Logger logger = LoggerFactory.getLogger(CacheUploader.class); + + private final MinioClient minioClient; + private final String minioBucket; + private final Archive archive; + + public CacheUploader(MinioClient minioClient, String minioBucket, Archive archive) + { + this.minioClient = minioClient; + this.minioBucket = minioBucket; + this.archive = archive; + } + + @Override + public void run() + { + byte[] data = archive.getData(); + String path = new StringBuilder() + .append(archive.getIndex().getId()) + .append('/') + .append(archive.getArchiveId()) + .append('/') + .append(archive.getRevision()) + .toString(); + + try + { + try (InputStream in = minioClient.getObject(minioBucket, path)) + { + return; // already exists + } + catch (ErrorResponseException ex) + { + // doesn't exist + } + + minioClient.putObject(minioBucket, path, new ByteArrayInputStream(data), data.length, "binary/octet-stream"); + } + catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidArgumentException | InvalidBucketNameException | NoResponseException | IOException | InvalidKeyException | NoSuchAlgorithmException | XmlPullParserException ex) + { + logger.warn("unable to upload data to store", ex); + } + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/NotFoundException.java b/http-service/src/main/java/net/runelite/http/service/cache/NotFoundException.java new file mode 100644 index 0000000000..4787f0f859 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/NotFoundException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not found") +public class NotFoundException extends RuntimeException +{ + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/OutOfDateException.java b/http-service/src/main/java/net/runelite/http/service/cache/OutOfDateException.java new file mode 100644 index 0000000000..e3c4d7b32c --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/OutOfDateException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache; + +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(reason = "Out of date") +public class OutOfDateException extends RuntimeException +{ + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java new file mode 100644 index 0000000000..1a9d0191b2 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/ArchiveEntry.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache.beans; + +public class ArchiveEntry +{ + private int id; + private int archiveId; + private int nameHash; + private int revision; + + @Override + public String toString() + { + return "ArchiveEntry{" + "id=" + id + ", archiveId=" + archiveId + ", nameHash=" + nameHash + ", revision=" + revision + '}'; + } + + @Override + public int hashCode() + { + int hash = 7; + hash = 89 * hash + this.id; + hash = 89 * hash + this.archiveId; + hash = 89 * hash + this.nameHash; + hash = 89 * hash + this.revision; + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final ArchiveEntry other = (ArchiveEntry) obj; + if (this.id != other.id) + { + return false; + } + if (this.archiveId != other.archiveId) + { + return false; + } + if (this.nameHash != other.nameHash) + { + return false; + } + if (this.revision != other.revision) + { + return false; + } + return true; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getArchiveId() + { + return archiveId; + } + + public void setArchiveId(int archiveId) + { + this.archiveId = archiveId; + } + + public int getNameHash() + { + return nameHash; + } + + public void setNameHash(int nameHash) + { + this.nameHash = nameHash; + } + + public int getRevision() + { + return revision; + } + + public void setRevision(int revision) + { + this.revision = revision; + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java new file mode 100644 index 0000000000..050a42688b --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/CacheEntry.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache.beans; + +import java.time.Instant; +import java.util.Objects; + +public class CacheEntry +{ + private int id; + private int revision; + private Instant date; + + @Override + public String toString() + { + return "CacheEntry{" + "id=" + id + ", revision=" + revision + ", date=" + date + '}'; + } + + @Override + public int hashCode() + { + int hash = 7; + hash = 23 * hash + this.id; + hash = 23 * hash + this.revision; + hash = 23 * hash + Objects.hashCode(this.date); + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final CacheEntry other = (CacheEntry) obj; + if (this.id != other.id) + { + return false; + } + if (this.revision != other.revision) + { + return false; + } + if (!Objects.equals(this.date, other.date)) + { + return false; + } + return true; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getRevision() + { + return revision; + } + + public void setRevision(int revision) + { + this.revision = revision; + } + + public Instant getDate() + { + return date; + } + + public void setDate(Instant date) + { + this.date = date; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java new file mode 100644 index 0000000000..ee57712e0f --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/FileEntry.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache.beans; + +public class FileEntry +{ + private int id; + private int archiveId; + private int fileId; + private int nameHash; + + @Override + public String toString() + { + return "FileEntry{" + "id=" + id + ", archiveId=" + archiveId + ", fileId=" + fileId + ", nameHash=" + nameHash + '}'; + } + + @Override + public int hashCode() + { + int hash = 5; + hash = 67 * hash + this.id; + hash = 67 * hash + this.archiveId; + hash = 67 * hash + this.fileId; + hash = 67 * hash + this.nameHash; + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final FileEntry other = (FileEntry) obj; + if (this.id != other.id) + { + return false; + } + if (this.archiveId != other.archiveId) + { + return false; + } + if (this.fileId != other.fileId) + { + return false; + } + if (this.nameHash != other.nameHash) + { + return false; + } + return true; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getArchiveId() + { + return archiveId; + } + + public void setArchiveId(int archiveId) + { + this.archiveId = archiveId; + } + + public int getFileId() + { + return fileId; + } + + public void setFileId(int fileId) + { + this.fileId = fileId; + } + + public int getNameHash() + { + return nameHash; + } + + public void setNameHash(int nameHash) + { + this.nameHash = nameHash; + } + +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java b/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java new file mode 100644 index 0000000000..be67820b01 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/beans/IndexEntry.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.cache.beans; + +public class IndexEntry +{ + private int id; + private int indexId; + private int revision; + + @Override + public String toString() + { + return "IndexEntry{" + "id=" + id + ", indexId=" + indexId + ", revision=" + revision + '}'; + } + + @Override + public int hashCode() + { + int hash = 5; + hash = 83 * hash + this.id; + hash = 83 * hash + this.indexId; + hash = 83 * hash + this.revision; + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final IndexEntry other = (IndexEntry) obj; + if (this.id != other.id) + { + return false; + } + if (this.indexId != other.indexId) + { + return false; + } + if (this.revision != other.revision) + { + return false; + } + return true; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public int getIndexId() + { + return indexId; + } + + public void setIndexId(int indexId) + { + this.indexId = indexId; + } + + public int getRevision() + { + return revision; + } + + public void setRevision(int revision) + { + this.revision = revision; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/cache/schema.sql b/http-service/src/main/java/net/runelite/http/service/cache/schema.sql new file mode 100644 index 0000000000..3032134734 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/cache/schema.sql @@ -0,0 +1,132 @@ +-- MySQL dump 10.16 Distrib 10.2.8-MariaDB, for osx10.12 (x86_64) +-- +-- Host: localhost Database: cache +-- ------------------------------------------------------ +-- Server version 10.2.8-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `archive` +-- + +DROP TABLE IF EXISTS `archive`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `archive` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `archiveId` int(11) NOT NULL, + `nameHash` int(11) NOT NULL, + `revision` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `archive_revision` (`archiveId`,`revision`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cache` +-- + +DROP TABLE IF EXISTS `cache`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cache` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `revision` int(11) NOT NULL, + `date` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `revision_date` (`revision`,`date`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `cache_index` +-- + +DROP TABLE IF EXISTS `cache_index`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cache_index` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `cache` int(11) NOT NULL, + `index` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `cacheId` (`cache`), + KEY `indexId` (`index`), + CONSTRAINT `cache_index_ibfk_1` FOREIGN KEY (`cache`) REFERENCES `cache` (`id`), + CONSTRAINT `cache_index_ibfk_2` FOREIGN KEY (`index`) REFERENCES `index` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `file` +-- + +DROP TABLE IF EXISTS `file`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `file` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `archive` int(11) NOT NULL, + `fileId` int(11) NOT NULL, + `nameHash` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `archive_file` (`archive`,`fileId`), + CONSTRAINT `file_ibfk_1` FOREIGN KEY (`archive`) REFERENCES `archive` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `index` +-- + +DROP TABLE IF EXISTS `index`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `index` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `indexId` int(11) NOT NULL, + `revision` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `indexId` (`indexId`,`revision`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `index_archive` +-- + +DROP TABLE IF EXISTS `index_archive`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `index_archive` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `index` int(11) NOT NULL, + `archive` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `index` (`index`) USING BTREE, + KEY `archive` (`archive`) USING BTREE, + CONSTRAINT `index_archive_ibfk_1` FOREIGN KEY (`index`) REFERENCES `index` (`id`), + CONSTRAINT `index_archive_ibfk_2` FOREIGN KEY (`archive`) REFERENCES `archive` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2017-09-10 13:11:23 diff --git a/http-service/src/test/java/net/runelite/http/service/SpringBootWebApplicationTest.java b/http-service/src/test/java/net/runelite/http/service/SpringBootWebApplicationTest.java index a957150b07..865363ad51 100644 --- a/http-service/src/test/java/net/runelite/http/service/SpringBootWebApplicationTest.java +++ b/http-service/src/test/java/net/runelite/http/service/SpringBootWebApplicationTest.java @@ -27,6 +27,7 @@ package net.runelite.http.service; import java.time.Instant; import java.util.HashMap; import java.util.Map; +import javax.naming.NamingException; import org.junit.Ignore; import org.junit.Test; import org.springframework.boot.SpringApplication; @@ -44,8 +45,15 @@ public class SpringBootWebApplicationTest { Map converters = new HashMap<>(); converters.put(Instant.class, new InstantConverter()); - return new Sql2o("jdbc:mysql://192.168.1.2/runelite", "adam", "", new NoQuirks(converters)); + return new Sql2o("jdbc:mysql://localhost/runelite", "root", "", new NoQuirks(converters)); + } + @Bean("Runelite Cache SQL2O") + Sql2o cacheSql2o() throws NamingException + { + Map converters = new HashMap<>(); + converters.put(Instant.class, new InstantConverter()); + return new Sql2o("jdbc:mysql://localhost/cache", "root", "", new NoQuirks(converters)); } @Test diff --git a/http-service/src/test/resources/application.properties b/http-service/src/test/resources/application.properties index 27cffd3487..bdaf64e9db 100644 --- a/http-service/src/test/resources/application.properties +++ b/http-service/src/test/resources/application.properties @@ -1,2 +1,7 @@ oauth.client-id=moo -oauth.client-secret=cow \ No newline at end of file +oauth.client-secret=cow +minio.endpoint=http://10.96.22.171:9000 +minio.accesskey=AM54M27O4WZK65N6F8IP +minio.secretkey=/PZCxzmsJzwCHYlogcymuprniGCaaLUOET2n6yMP +minio.bucket=runelite +auth.password=password