integration tests added
This commit is contained in:
parent
62d4eaf65d
commit
a7998082a7
28 changed files with 471 additions and 46 deletions
27
build.gradle
27
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
|
||||
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> response = restTemplate().getForEntity(urlWithPort("/c/blah-blah"), byte[]);
|
||||
|
||||
then:
|
||||
response.statusCode == HttpStatus.NOT_FOUND
|
||||
}
|
||||
}
|
|
@ -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<byte[]> 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<String> 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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<ConfigurableWebServerFactory> {
|
||||
@Value("\${random.int(9090,65535)}")
|
||||
int port
|
||||
|
||||
int getPort() {
|
||||
return port
|
||||
}
|
||||
|
||||
@Override
|
||||
void customize(ConfigurableWebServerFactory factory) {
|
||||
factory.setPort(port);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
19
src/integration/resources/application-integration.yml
Normal file
19
src/integration/resources/application-integration.yml
Normal file
|
@ -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
|
||||
|
4
src/integration/resources/application.yml
Normal file
4
src/integration/resources/application.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
spring:
|
||||
profiles:
|
||||
active: integration
|
||||
|
58
src/integration/resources/db/data.sql
Normal file
58
src/integration/resources/db/data.sql
Normal file
|
@ -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');
|
76
src/integration/resources/db/schema.sql
Normal file
76
src/integration/resources/db/schema.sql
Normal file
|
@ -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`);
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 580 KiB |
Binary file not shown.
After Width: | Height: | Size: 447 KiB |
Binary file not shown.
After Width: | Height: | Size: 464 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
|
@ -37,7 +37,7 @@ class DlnaService(
|
|||
private val mediaServer: MediaServer,
|
||||
private val serverInfoProvider: ServerInfoProvider,
|
||||
) {
|
||||
private val addressesToBind: List<InetAddress> = listOf(serverInfoProvider.address!!)
|
||||
private val addressesToBind: List<String> = 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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -115,7 +115,7 @@ class ApacheStreamClient(
|
|||
): Callable<StreamResponseMessage> {
|
||||
return Callable<StreamResponseMessage> {
|
||||
logger.trace("Sending HTTP request: $requestMessage")
|
||||
httpClient.execute<StreamResponseMessage>(request, createResponseHandler(requestMessage))
|
||||
httpClient.execute(request, createResponseHandler())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ class ApacheStreamClient(
|
|||
clientConnectionManager.shutdown()
|
||||
}
|
||||
|
||||
private fun createResponseHandler(requestMessage: StreamRequestMessage?): ResponseHandler<StreamResponseMessage> {
|
||||
private fun createResponseHandler(): ResponseHandler<StreamResponseMessage> {
|
||||
return ResponseHandler<StreamResponseMessage> { response ->
|
||||
val statusLine = response.statusLine
|
||||
logger.trace("Received HTTP response: $statusLine")
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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}"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue