diff --git a/build.gradle b/build.gradle index cd6f46b..4bc09c8 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,11 @@ repositories { mavenCentral() } +configurations { + integrationTestImplementation.extendsFrom(testImplementation) + integrationTestRuntimeOnly.extendsFrom(testRuntimeOnly) +} + dependencies { implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' @@ -41,13 +46,14 @@ dependencies { implementation 'org.jupnp:org.jupnp.support:2.7.1' implementation 'org.apache.httpcomponents:httpclient:4.5.14' - implementation 'org.apache.groovy:groovy:4.0.15' + testImplementation 'org.apache.groovy:groovy:4.0.15' testImplementation('org.spockframework:spock-core:2.4-M1-groovy-4.0') testImplementation('org.spockframework:spock-spring:2.4-M1-groovy-4.0') testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } + testImplementation('com.h2database:h2') } tasks.withType(KotlinCompile).configureEach { @@ -62,17 +68,22 @@ tasks.named('test') { } sourceSets { - integration { - java { - compileClasspath += main.output + test.output - runtimeClasspath += main.output + test.output - } + integrationTest { + groovy.srcDir "$projectDir/src/integration/groovy" + resources.srcDir "$projectDir/src/integration/resources" + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output } } tasks.register('integrationTest', Test) { + useJUnitPlatform() description = "Run integration tests" group = "verification" - testClassesDirs = sourceSets.integration.output.classesDirs - classpath = sourceSets.integration.runtimeClasspath + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + mustRunAfter tasks.test } + +check.dependsOn integrationTest + diff --git a/src/integration/groovy/net/schowek/nextclouddlna/controller/ContentControllerIntTest.groovy b/src/integration/groovy/net/schowek/nextclouddlna/controller/ContentControllerIntTest.groovy new file mode 100644 index 0000000..2d33c38 --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/controller/ContentControllerIntTest.groovy @@ -0,0 +1,33 @@ +package net.schowek.nextclouddlna.controller + + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import support.IntegrationSpecification + +class ContentControllerIntTest extends IntegrationSpecification { + + def "should process GET request for content"() { + when: + ResponseEntity response = restTemplate().getForEntity(urlWithPort("/c/19"), byte[]); + + then: + response.statusCode == HttpStatus.OK + with(response.headers.each { it.key.toLowerCase() }) { + assert it['content-type'] == ['image/jpeg'] + assert it['accept-ranges'] == ["bytes"] + assert it.containsKey('contentfeatures.dlna.org') + assert it.containsKey('transfermode.dlna.org') + assert it.containsKey('realtimeinfo.dlna.org') + } + response.body.length == 2170375 + } + + def "should return 404 if content does not exist"() { + when: + ResponseEntity response = restTemplate().getForEntity(urlWithPort("/c/blah-blah"), byte[]); + + then: + response.statusCode == HttpStatus.NOT_FOUND + } +} diff --git a/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy b/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy new file mode 100644 index 0000000..821e732 --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/controller/UpnpControllerIntTest.groovy @@ -0,0 +1,65 @@ +package net.schowek.nextclouddlna.controller + +import net.schowek.nextclouddlna.dlna.media.MediaServer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.xml.sax.InputSource +import support.IntegrationSpecification + +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +import static javax.xml.xpath.XPathConstants.NODE + +class UpnpControllerIntTest extends IntegrationSpecification { + + @Autowired + private MediaServer mediaServer + + def "should serve icon"() { + given: + def uid = mediaServer.serviceIdentifier + + when: + ResponseEntity response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/icon.png"), byte[]); + + then: + response.statusCode == HttpStatus.OK + with(response.headers.each { it.key.toLowerCase() }) { + assert it['content-type'] == ['application/octet-stream'] + } + } + + def "should serve service descriptor"() { + given: + def uid = mediaServer.serviceIdentifier + + when: + ResponseEntity response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/desc"), String); + + then: + response.statusCode == HttpStatus.OK + with(response.headers.each { it.key.toLowerCase() }) { + assert it['content-type'] == ['text/xml'] + } + + Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( + new InputSource(new StringReader(response.body)) + ); + + then: + nodeValue(dom, "/root/device/friendlyName") == "nextcloud-dlna-int-test" + nodeValue(dom, "/root/device/UDN") == "uuid:${uid}" + nodeValue(dom, "/root/device/presentationURL") == urlWithPort() + } + + private String nodeValue(Document dom, String pattern) { + XPath xpath = XPathFactory.newInstance().newXPath(); + return (xpath.evaluate("$pattern/text()", dom, NODE) as Node).nodeValue + } +} diff --git a/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProviderInt.groovy b/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProviderInt.groovy new file mode 100644 index 0000000..2bd3b98 --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProviderInt.groovy @@ -0,0 +1,14 @@ +package net.schowek.nextclouddlna.nextcloud.config + + +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component + +@Component +@Profile("integration") +class NextcloudAppPathProviderInt implements NextcloudAppPathProvider { + @Override + File getNextcloudDir() { + return new File(getClass().getResource("/nextcloud/app/data").getFile()) + } +} diff --git a/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderIntTest.groovy b/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderIntTest.groovy new file mode 100644 index 0000000..83bedd3 --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/nextcloud/content/ContentTreeProviderIntTest.groovy @@ -0,0 +1,17 @@ +package net.schowek.nextclouddlna.nextcloud.content + +import org.springframework.beans.factory.annotation.Autowired +import support.IntegrationSpecification + +class ContentTreeProviderIntTest extends IntegrationSpecification { + @Autowired + ContentTreeProvider contentTreeProvider + + def "should foo"() { + when: + def result = contentTreeProvider.getItem("19") + + then: + result.id == 19 + } +} diff --git a/src/integration/groovy/net/schowek/nextclouddlna/util/ServerInfoProviderInt.groovy b/src/integration/groovy/net/schowek/nextclouddlna/util/ServerInfoProviderInt.groovy new file mode 100644 index 0000000..1a30d66 --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/util/ServerInfoProviderInt.groovy @@ -0,0 +1,26 @@ +package net.schowek.nextclouddlna.util + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component + +@Component +@Profile("integration") +class ServerInfoProviderInt implements ServerInfoProvider { + private final ServerPortCustomizer serverPortCustomizer + + @Autowired + ServerInfoProviderInt(ServerPortCustomizer serverPortCustomizer) { + this.serverPortCustomizer = serverPortCustomizer + } + + @Override + String getHost() { + return "localhost" + } + + @Override + int getPort() { + return serverPortCustomizer.port + } +} diff --git a/src/integration/groovy/net/schowek/nextclouddlna/util/ServerPortCustomizer.groovy b/src/integration/groovy/net/schowek/nextclouddlna/util/ServerPortCustomizer.groovy new file mode 100644 index 0000000..4c997ad --- /dev/null +++ b/src/integration/groovy/net/schowek/nextclouddlna/util/ServerPortCustomizer.groovy @@ -0,0 +1,23 @@ +package net.schowek.nextclouddlna.util + +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.web.server.ConfigurableWebServerFactory +import org.springframework.boot.web.server.WebServerFactoryCustomizer +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component + +@Component +@Profile("integration") +public class ServerPortCustomizer implements WebServerFactoryCustomizer { + @Value("\${random.int(9090,65535)}") + int port + + int getPort() { + return port + } + + @Override + void customize(ConfigurableWebServerFactory factory) { + factory.setPort(port); + } +} \ No newline at end of file diff --git a/src/integration/groovy/support/IntegrationSpecification.groovy b/src/integration/groovy/support/IntegrationSpecification.groovy new file mode 100644 index 0000000..a3e5f81 --- /dev/null +++ b/src/integration/groovy/support/IntegrationSpecification.groovy @@ -0,0 +1,35 @@ +package support + +import net.schowek.nextclouddlna.NextcloudDLNAApp +import net.schowek.nextclouddlna.util.ServerInfoProvider +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT + +@ContextConfiguration(loader = SpringBootContextLoader, classes = NextcloudDLNAApp.class) +@SpringBootTest(webEnvironment = DEFINED_PORT) +@ActiveProfiles("integration") +class IntegrationSpecification extends Specification { + private TestRestTemplate restTemplate = new TestRestTemplate() + + // TODO BEAN + TestRestTemplate restTemplate() { + if (restTemplate == null) { + restTemplate = new TestRestTemplate() + } + return restTemplate + } + + @Autowired + private ServerInfoProvider serverInfoProvider + + protected String urlWithPort(String uri = "") { + return "http://localhost:" + serverInfoProvider.port + uri; + } +} diff --git a/src/integration/resources/application-integration.yml b/src/integration/resources/application-integration.yml new file mode 100644 index 0000000..dee3d54 --- /dev/null +++ b/src/integration/resources/application-integration.yml @@ -0,0 +1,19 @@ +server: + friendlyName: "nextcloud-dlna-int-test" + +spring: + datasource: + url: jdbc:h2:mem:nextcloud-dlna-integration + username: sa + password: + driverClassName: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: none + sql: + init: + mode: always + schema-locations: classpath:db/schema.sql + data-locations: classpath:db/data.sql + diff --git a/src/integration/resources/application.yml b/src/integration/resources/application.yml new file mode 100644 index 0000000..3338026 --- /dev/null +++ b/src/integration/resources/application.yml @@ -0,0 +1,4 @@ +spring: + profiles: + active: integration + diff --git a/src/integration/resources/db/data.sql b/src/integration/resources/db/data.sql new file mode 100644 index 0000000..21da1a5 --- /dev/null +++ b/src/integration/resources/db/data.sql @@ -0,0 +1,58 @@ + +INSERT INTO `oc_appconfig` VALUES +('groupfolders','enabled','yes'), +('groupfolders','installed_version','15.3.1'), +('groupfolders','types','filesystem,dav'); + +INSERT INTO `oc_filecache` VALUES +(1,1,'','d41d8cd98f00b204e9800998ecf8427e',-1,'',2,1,5341566992,1696704138,1696613362,0,0,'',23,''), +(2,1,'files','45b963397aa40d4a0063e0d85e4fe7a1',1,'files',2,1,5341560694,1696704138,1696704138,0,0,'',31,''), +(3,2,'','d41d8cd98f00b204e9800998ecf8427e',-1,'',2,1,13286908,1696702221,1696695204,0,0,'',23,''), +(4,2,'appdata_integration','bed7fa8a60170b5d88c9da5e69eaeb5a',3,'appdata_integration',2,1,10274496,1695737790,1695737790,0,0,'',31,''), +(13,1,'files/Nextcloud intro.mp4','e4919345bcc87d4585a5525daaad99c0',2,'Nextcloud intro.mp4',9,8,3963036,1695737656,1695737656,0,0,'',27,''), +(14,1,'files/Nextcloud.png','',2,'Nextcloud.png',11,10,50598,1695737656,1695737656,0,0,'',27,''), +(15,1,'files/Photos','d01bb67e7b71dd49fd06bad922f521c9',2,'Photos',2,1,5656462,1695737827,1695737658,0,0,'',31,''), +(16,1,'files/Photos/Birdie.jpg','cd31c7af3a0ec6e15782b5edd2774549',15,'Birdie.jpg',12,10,593508,1695737656,1695737656,0,0,'',27,''), +(17,1,'files/Photos/Frog.jpg','d6219add1a9129ed0c1513af985e2081',15,'Frog.jpg',12,10,457744,1695737656,1695737656,0,0,'',27,''), +(18,1,'files/Photos/Gorilla.jpg','6d5f5956d8ff76a5f290cebb56402789',15,'Gorilla.jpg',12,10,474653,1695737656,1695737656,0,0,'',27,''), +(19,1,'files/Photos/Library.jpg','0b785d02a19fc00979f82f6b54a05805',15,'Library.jpg',12,10,2170375,1695737657,1695737657,0,0,'',27,''), +(20,1,'files/Photos/Nextcloud community.jpg','b9b3caef83a2a1c20354b98df6bcd9d0',15,'Nextcloud community.jpg',12,10,797325,1695737657,1695737657,0,0, +'',27,''), +(22,1,'files/Photos/Steps.jpg','7b2ca8d05bbad97e00cbf5833d43e912',15,'Steps.jpg',12,10,567689,1695737658,1695737658,0,0,'',27,''), +(23,1,'files/Photos/Toucan.jpg','681d1e78f46a233e12ecfa722cbc2aef',15,'Toucan.jpg',12,10,167989,1695737658,1695737658,0,0,'',27,''), +(24,1,'files/Photos/Vineyard.jpg','14e5f2670b0817614acd52269d971db8',15,'Vineyard.jpg',12,10,427030,1695737658,1695737658,0,0,'',27,''), +(69,2,'appdata_integration/preview','e771733d5f59ead277f502588282d693',4,'preview',2,1,5153144,1695738765,1695738765,0,0,'',31,''); + +INSERT INTO `oc_group_folders` VALUES +(1,'family folder',-3,0); + +INSERT INTO `oc_mimetypes` VALUES +(5,'application'), +(19,'application/gzip'), +(18,'application/javascript'), +(20,'application/json'), +(16,'application/octet-stream'), +(6,'application/pdf'), +(13,'application/vnd.oasis.opendocument.graphics'), +(15,'application/vnd.oasis.opendocument.presentation'), +(14,'application/vnd.oasis.opendocument.spreadsheet'), +(17,'application/vnd.oasis.opendocument.text'), +(7,'application/vnd.openxmlformats-officedocument.wordprocessingml.document'), +(22,'audio'), +(23,'audio/mpeg'), +(1,'httpd'), +(2,'httpd/unix-directory'), +(10,'image'), +(12,'image/jpeg'), +(11,'image/png'), +(21,'image/svg+xml'), +(3,'text'), +(4,'text/markdown'), +(8,'video'), +(9,'video/mp4'); + +INSERT INTO `oc_mounts` VALUES +(1,1,1,'johndoe','/johndoe/',NULL,'OC\\Files\\Mount\\LocalHomeMountProvider'), +(2,3,384,'janedoe','/janedoe/',NULL,'OC\\Files\\Mount\\LocalHomeMountProvider'), +(3,2,586,'johndoe','/johndoe/files/family folder/',NULL,'OCA\\GroupFolders\\Mount\\MountProvider'), +(4,2,586,'janedoe','/janedoe/files/family folder/',NULL,'OCA\\GroupFolders\\Mount\\MountProvider'); diff --git a/src/integration/resources/db/schema.sql b/src/integration/resources/db/schema.sql new file mode 100644 index 0000000..9511c56 --- /dev/null +++ b/src/integration/resources/db/schema.sql @@ -0,0 +1,76 @@ +DROP TABLE IF EXISTS oc_appconfig; +CREATE TABLE `oc_appconfig` ( + `appid` varchar(32) NOT NULL DEFAULT '', + `configkey` varchar(64) NOT NULL DEFAULT '', + `configvalue` longtext DEFAULT NULL, + PRIMARY KEY (`appid`,`configkey`) +); + +CREATE INDEX `appconfig_config_key_index` ON oc_appconfig(`configkey`); + +DROP TABLE IF EXISTS oc_filecache; +CREATE TABLE `oc_filecache` ( + `fileid` NUMERIC(20) NOT NULL AUTO_INCREMENT, + `storage` NUMERIC(20) NOT NULL DEFAULT 0, + `path` varchar(4000) DEFAULT NULL, + `path_hash` varchar(32) NOT NULL DEFAULT '', + `parent` NUMERIC(20) NOT NULL DEFAULT 0, + `name` varchar(250) DEFAULT NULL, + `mimetype` NUMERIC(20) NOT NULL DEFAULT 0, + `mimepart` NUMERIC(20) NOT NULL DEFAULT 0, + `size` NUMERIC(20) NOT NULL DEFAULT 0, + `mtime` NUMERIC(20) NOT NULL DEFAULT 0, + `storage_mtime` NUMERIC(20) NOT NULL DEFAULT 0, + `encrypted` INTEGER NOT NULL DEFAULT 0, + `unencrypted_size` NUMERIC(20) NOT NULL DEFAULT 0, + `etag` varchar(40) DEFAULT NULL, + `permissions` INTEGER DEFAULT 0, + `checksum` varchar(255) DEFAULT NULL, + PRIMARY KEY (`fileid`) +); +CREATE UNIQUE INDEX `fs_storage_path_hash` ON oc_filecache(`storage`,`path_hash`); +CREATE INDEX `fs_parent_name_hash` ON oc_filecache(`parent`,`name`); +CREATE INDEX `fs_storage_mimetype` ON oc_filecache(`storage`,`mimetype`); +CREATE INDEX `fs_storage_mimepart` ON oc_filecache(`storage`,`mimepart`); +CREATE INDEX `fs_storage_size` ON oc_filecache(`storage`,`size`,`fileid`); +CREATE INDEX `fs_id_storage_size` ON oc_filecache(`fileid`,`storage`,`size`); +CREATE INDEX `fs_parent` ON oc_filecache(`parent`); +CREATE INDEX `fs_mtime` ON oc_filecache(`mtime`); +CREATE INDEX `fs_size` ON oc_filecache(`size`); +CREATE INDEX `fs_storage_path_prefix` ON oc_filecache(`storage`,`path`); + +DROP TABLE IF EXISTS oc_group_folders; +CREATE TABLE `oc_group_folders` ( + `folder_id` NUMERIC(20) NOT NULL AUTO_INCREMENT, + `mount_point` varchar(4000) NOT NULL, + `quota` NUMERIC(20) NOT NULL DEFAULT -3, + `acl` INTEGER DEFAULT 0, + PRIMARY KEY (`folder_id`) +); + +DROP TABLE IF EXISTS oc_mimetypes; +CREATE TABLE `oc_mimetypes` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `mimetype` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +); + +CREATE UNIQUE INDEX `mimetype_id_index` ON oc_mimetypes(`mimetype`); + +DROP TABLE IF EXISTS oc_mounts; +CREATE TABLE `oc_mounts` ( + `id` NUMERIC(20) NOT NULL AUTO_INCREMENT, + `storage_id` NUMERIC(20) NOT NULL, + `root_id` NUMERIC(20) NOT NULL, + `user_id` varchar(64) NOT NULL, + `mount_point` varchar(4000) NOT NULL, + `mount_id` NUMERIC(20) DEFAULT NULL, + `mount_provider_class` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`) +); +CREATE INDEX `mounts_storage_index` ON oc_mounts(`storage_id`); +CREATE INDEX `mounts_root_index` ON oc_mounts(`root_id`); +CREATE INDEX `mounts_mount_id_index` ON oc_mounts(`mount_id`); +CREATE INDEX `mounts_user_root_path_index` ON oc_mounts(`user_id`,`root_id`,`mount_point`); +CREATE INDEX `mounts_class_index` ON oc_mounts(`mount_provider_class`); +CREATE INDEX `mount_user_storage` ON oc_mounts(`storage_id`,`user_id`); diff --git a/src/integration/resources/nextcloud/app/data/johndoe/files/Nextcloud intro.mp4 b/src/integration/resources/nextcloud/app/data/johndoe/files/Nextcloud intro.mp4 new file mode 100644 index 0000000..846f982 Binary files /dev/null and b/src/integration/resources/nextcloud/app/data/johndoe/files/Nextcloud intro.mp4 differ diff --git a/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Birdie.jpg b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Birdie.jpg new file mode 100644 index 0000000..dd11a57 Binary files /dev/null and b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Birdie.jpg differ diff --git a/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Frog.jpg b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Frog.jpg new file mode 100644 index 0000000..d309ab0 Binary files /dev/null and b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Frog.jpg differ diff --git a/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Gorilla.jpg b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Gorilla.jpg new file mode 100644 index 0000000..c7a204b Binary files /dev/null and b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Gorilla.jpg differ diff --git a/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Library.jpg b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Library.jpg new file mode 100644 index 0000000..61e6c19 Binary files /dev/null and b/src/integration/resources/nextcloud/app/data/johndoe/files/photos/Library.jpg differ diff --git a/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt b/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt index 8453f8d..a9a4f64 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/DllnaService.kt @@ -37,7 +37,7 @@ class DlnaService( private val mediaServer: MediaServer, private val serverInfoProvider: ServerInfoProvider, ) { - private val addressesToBind: List = listOf(serverInfoProvider.address!!) + private val addressesToBind: List = listOf(serverInfoProvider.host) var upnpService = MyUpnpService(MyUpnpServiceConfiguration()) fun start() { @@ -91,7 +91,7 @@ class DlnaService( multicastResponsePort: Int ) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) { override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) = - addressesToBind.contains(address) + addressesToBind.contains(address.hostAddress) } companion object : KLogging() diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/ContentController.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/ContentController.kt index 1993e13..63c627a 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/controller/ContentController.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/ContentController.kt @@ -38,11 +38,16 @@ class ContentController( logger.info("Serving content {} {}", request.method, id) } val fileSystemResource = FileSystemResource(item.path) - response.addHeader("Content-Type", item.format.mime) - response.addHeader("contentFeatures.dlna.org", makeProtocolInfo(item.format).toString()) - response.addHeader("transferMode.dlna.org", "Streaming") - response.addHeader("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*") - ResponseEntity(fileSystemResource, HttpStatus.OK) + if (!fileSystemResource.exists()) { + logger.info("Could not find file for item id: {}", id) + ResponseEntity(HttpStatus.NOT_FOUND) + } else { + response.addHeader("Content-Type", item.format.mime) + response.addHeader("contentFeatures.dlna.org", makeProtocolInfo(item.format).toString()) + response.addHeader("transferMode.dlna.org", "Streaming") + response.addHeader("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*") + ResponseEntity(fileSystemResource, HttpStatus.OK) + } } ?: let { logger.info("Could not find item id: {}", id) ResponseEntity(HttpStatus.NOT_FOUND) diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt index 819bd94..1ab20f4 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt @@ -21,18 +21,20 @@ class MediaServer( private val friendlyName: String, externalUrls: ExternalUrls ) { - final val device = LocalDevice( + val device = LocalDevice( DeviceIdentity(uniqueSystemIdentifier("Nextcloud-DLNA-MediaServer"), ADVERTISEMENT_AGE_IN_S), UDADeviceType(DEVICE_TYPE, VERSION), DeviceDetails(friendlyName, externalUrls.selfURI), createDeviceIcon(), arrayOf(contentDirectoryService, connectionManagerService) ) + val serviceIdentifier: String get() = device.identity.udn.identifierString init { logger.info("uniqueSystemIdentifier: {} ({})", device.identity.udn, friendlyName) } + companion object : KLogging() { const val ICON_FILENAME = "icon.png" private const val DEVICE_TYPE = "MediaServer" diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/ApacheStreamClient.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/ApacheStreamClient.kt index 0b44f92..863b7ce 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/ApacheStreamClient.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/ApacheStreamClient.kt @@ -115,7 +115,7 @@ class ApacheStreamClient( ): Callable { return Callable { logger.trace("Sending HTTP request: $requestMessage") - httpClient.execute(request, createResponseHandler(requestMessage)) + httpClient.execute(request, createResponseHandler()) } } @@ -141,7 +141,7 @@ class ApacheStreamClient( clientConnectionManager.shutdown() } - private fun createResponseHandler(requestMessage: StreamRequestMessage?): ResponseHandler { + private fun createResponseHandler(): ResponseHandler { return ResponseHandler { response -> val statusLine = response.statusLine logger.trace("Received HTTP response: $statusLine") diff --git a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt index b118fc8..f4efcd3 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudDB.kt @@ -2,6 +2,7 @@ package net.schowek.nextclouddlna.nextcloud import jakarta.annotation.PostConstruct import mu.KLogging +import net.schowek.nextclouddlna.nextcloud.config.NextcloudConfigDiscovery import net.schowek.nextclouddlna.nextcloud.content.ContentItem import net.schowek.nextclouddlna.nextcloud.content.ContentNode import net.schowek.nextclouddlna.nextcloud.content.MediaFormat @@ -96,9 +97,9 @@ class NextcloudDB( private fun buildPath(f: Filecache): String { return if (storageUsersMap.containsKey(f.storage)) { val userName: String? = storageUsersMap[f.storage] - "${nextcloudConfig.nextcloudDir}/$userName/${f.path}" + "${nextcloudConfig.nextcloudDir.absolutePath}/$userName/${f.path}" } else { - "${nextcloudConfig.nextcloudDir}/${f.path}" + "${nextcloudConfig.nextcloudDir.absolutePath}/${f.path}" } } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProvider.kt b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProvider.kt new file mode 100644 index 0000000..6528bbe --- /dev/null +++ b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudAppPathProvider.kt @@ -0,0 +1,30 @@ +package net.schowek.nextclouddlna.nextcloud.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import java.io.File + +interface NextcloudAppPathProvider { + val nextcloudDir: File +} + +@Component +@Profile("!integration") +class NextcloudAppPathsProviderImpl( + @Value("\${nextcloud.filesDir}") + private val dirPath: String, +) : NextcloudAppPathProvider { + override val nextcloudDir = ensureNextcloudDir(dirPath) + + private fun ensureNextcloudDir(dirPath: String): File { + if (dirPath.isEmpty()) { + throw RuntimeException("No nextcloud data directory name provided") + } + return File(dirPath).also { + if (!it.exists() || !it.isDirectory) { + throw RuntimeException("Invalid nextcloud data directory specified") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudConfigDiscovery.kt b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudConfigDiscovery.kt similarity index 61% rename from src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudConfigDiscovery.kt rename to src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudConfigDiscovery.kt index 87d6590..5f3997e 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/NextcloudConfigDiscovery.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/nextcloud/config/NextcloudConfigDiscovery.kt @@ -1,31 +1,28 @@ -package net.schowek.nextclouddlna.nextcloud +package net.schowek.nextclouddlna.nextcloud.config -import jakarta.annotation.PostConstruct import mu.KLogging import net.schowek.nextclouddlna.nextcloud.db.AppConfigRepository -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.io.File -import java.util.* import java.util.Arrays.* import java.util.Objects.* @Component class NextcloudConfigDiscovery( - @Value("\${nextcloud.filesDir}") - val nextcloudDir: String, - val appConfigRepository: AppConfigRepository + val appConfigRepository: AppConfigRepository, + val nextcloudDirProvider: NextcloudAppPathProvider ) { - final val appDataDir: String = findAppDataDir() - final val supportsGroupFolders: Boolean = checkGroupFoldersSupport() + val appDataDir: String = findAppDataDir() + val nextcloudDir: File get() = nextcloudDirProvider.nextcloudDir + val supportsGroupFolders: Boolean = checkGroupFoldersSupport() private fun checkGroupFoldersSupport(): Boolean { return "yes" == appConfigRepository.getValue("groupfolders", "enabled") } private fun findAppDataDir(): String { - return stream(requireNonNull(File(nextcloudDir).listFiles { f -> + return stream(requireNonNull(nextcloudDir.listFiles { f -> f.isDirectory && f.name.matches(APPDATA_NAME_PATTERN.toRegex()) })).findFirst().orElseThrow().name .also { diff --git a/src/main/kotlin/net/schowek/nextclouddlna/util/ExternalUrls.kt b/src/main/kotlin/net/schowek/nextclouddlna/util/ExternalUrls.kt index bbd6ac5..cfbe13c 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/util/ExternalUrls.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/util/ExternalUrls.kt @@ -10,8 +10,8 @@ class ExternalUrls( ) { val selfUriString: String = when (serverInfoProvider.port) { - 80 -> "http://${serverInfoProvider.address!!.hostAddress}" - else -> "http://${serverInfoProvider.address!!.hostAddress}:${serverInfoProvider.port}" + 80 -> "http://${serverInfoProvider.host}" + else -> "http://${serverInfoProvider.host}:${serverInfoProvider.port}" } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/util/ServerInfoProvider.kt b/src/main/kotlin/net/schowek/nextclouddlna/util/ServerInfoProvider.kt index 4d85be8..18c155a 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/util/ServerInfoProvider.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/util/ServerInfoProvider.kt @@ -3,15 +3,23 @@ package net.schowek.nextclouddlna.util import jakarta.annotation.PostConstruct import mu.KLogging import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component import java.net.* import java.util.* +interface ServerInfoProvider { + val host: String + val port: Int +} + @Component -class ServerInfoProvider( - @param:Value("\${server.port}") val port: Int, +@Profile("!integration") +class ServerInfoProviderImpl( + @param:Value("\${server.port}") override val port: Int, @param:Value("\${server.interface}") private val networkInterface: String -) { +) : ServerInfoProvider { + override val host: String get() = address!!.hostAddress var address: InetAddress? = null @PostConstruct @@ -22,9 +30,11 @@ class ServerInfoProvider( private fun guessInetAddress(): InetAddress { return try { - val en0 = NetworkInterface.getByName(networkInterface).inetAddresses - while (en0.hasMoreElements()) { - val x = en0.nextElement() + val iface = NetworkInterface.getByName(networkInterface) + ?: throw RuntimeException("Could not find network interface $networkInterface") + val addresses = iface.inetAddresses + while (addresses.hasMoreElements()) { + val x = addresses.nextElement() if (x is Inet4Address) { return x } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 852ab82..0c8c146 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,7 +4,7 @@ server: friendlyName: ${NEXTCLOUD_DLNA_FRIENDLY_NAME:Nextcloud-DLNA} nextcloud: - filesDir: ${NEXTCLOUD_DATA_DIR:/path/to/your/nextcloud/dir/ending/with/data} + filesDir: ${NEXTCLOUD_DATA_DIR} spring: datasource: @@ -15,3 +15,7 @@ spring: jpa: hibernate: ddl-auto: none + sql: + init: + mode: never + diff --git a/src/test/groovy/net/schowek/nextclouddlna/util/ExternalUrlsTest.groovy b/src/test/groovy/net/schowek/nextclouddlna/util/ExternalUrlsTest.groovy index bf3e3dc..e37d95e 100644 --- a/src/test/groovy/net/schowek/nextclouddlna/util/ExternalUrlsTest.groovy +++ b/src/test/groovy/net/schowek/nextclouddlna/util/ExternalUrlsTest.groovy @@ -3,17 +3,12 @@ package net.schowek.nextclouddlna.util import spock.lang.Specification class ExternalUrlsTest extends Specification { - def inetAddress = Mock(InetAddress) def serverInfoProvider = Mock(ServerInfoProvider) - def setup() { - serverInfoProvider.address >> inetAddress - } - def "should generate main url for the service"() { given: - inetAddress.getHostAddress() >> host serverInfoProvider.getPort() >> port + serverInfoProvider.getHost() >> host def sut = new ExternalUrls(serverInfoProvider) when: @@ -30,8 +25,8 @@ class ExternalUrlsTest extends Specification { def "should generate content urls"() { given: - inetAddress.getHostAddress() >> host serverInfoProvider.getPort() >> port + serverInfoProvider.getHost() >> host def sut = new ExternalUrls(serverInfoProvider) when: