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()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
integrationTestImplementation.extendsFrom(testImplementation)
|
||||||
|
integrationTestRuntimeOnly.extendsFrom(testRuntimeOnly)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('org.springframework.boot:spring-boot-starter-web') {
|
implementation('org.springframework.boot:spring-boot-starter-web') {
|
||||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
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.jupnp:org.jupnp.support:2.7.1'
|
||||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
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-core:2.4-M1-groovy-4.0')
|
||||||
testImplementation('org.spockframework:spock-spring: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') {
|
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||||
}
|
}
|
||||||
|
testImplementation('com.h2database:h2')
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(KotlinCompile).configureEach {
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
|
@ -62,17 +68,22 @@ tasks.named('test') {
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
integration {
|
integrationTest {
|
||||||
java {
|
groovy.srcDir "$projectDir/src/integration/groovy"
|
||||||
compileClasspath += main.output + test.output
|
resources.srcDir "$projectDir/src/integration/resources"
|
||||||
runtimeClasspath += main.output + test.output
|
compileClasspath += main.output + test.output
|
||||||
}
|
runtimeClasspath += main.output + test.output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('integrationTest', Test) {
|
tasks.register('integrationTest', Test) {
|
||||||
|
useJUnitPlatform()
|
||||||
description = "Run integration tests"
|
description = "Run integration tests"
|
||||||
group = "verification"
|
group = "verification"
|
||||||
testClassesDirs = sourceSets.integration.output.classesDirs
|
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||||
classpath = sourceSets.integration.runtimeClasspath
|
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 mediaServer: MediaServer,
|
||||||
private val serverInfoProvider: ServerInfoProvider,
|
private val serverInfoProvider: ServerInfoProvider,
|
||||||
) {
|
) {
|
||||||
private val addressesToBind: List<InetAddress> = listOf(serverInfoProvider.address!!)
|
private val addressesToBind: List<String> = listOf(serverInfoProvider.host)
|
||||||
var upnpService = MyUpnpService(MyUpnpServiceConfiguration())
|
var upnpService = MyUpnpService(MyUpnpServiceConfiguration())
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
@ -91,7 +91,7 @@ class DlnaService(
|
||||||
multicastResponsePort: Int
|
multicastResponsePort: Int
|
||||||
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
|
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
|
||||||
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
|
||||||
addressesToBind.contains(address)
|
addressesToBind.contains(address.hostAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : KLogging()
|
companion object : KLogging()
|
||||||
|
|
|
@ -38,11 +38,16 @@ class ContentController(
|
||||||
logger.info("Serving content {} {}", request.method, id)
|
logger.info("Serving content {} {}", request.method, id)
|
||||||
}
|
}
|
||||||
val fileSystemResource = FileSystemResource(item.path)
|
val fileSystemResource = FileSystemResource(item.path)
|
||||||
response.addHeader("Content-Type", item.format.mime)
|
if (!fileSystemResource.exists()) {
|
||||||
response.addHeader("contentFeatures.dlna.org", makeProtocolInfo(item.format).toString())
|
logger.info("Could not find file for item id: {}", id)
|
||||||
response.addHeader("transferMode.dlna.org", "Streaming")
|
ResponseEntity(HttpStatus.NOT_FOUND)
|
||||||
response.addHeader("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*")
|
} else {
|
||||||
ResponseEntity(fileSystemResource, HttpStatus.OK)
|
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 {
|
} ?: let {
|
||||||
logger.info("Could not find item id: {}", id)
|
logger.info("Could not find item id: {}", id)
|
||||||
ResponseEntity(HttpStatus.NOT_FOUND)
|
ResponseEntity(HttpStatus.NOT_FOUND)
|
||||||
|
|
|
@ -21,18 +21,20 @@ class MediaServer(
|
||||||
private val friendlyName: String,
|
private val friendlyName: String,
|
||||||
externalUrls: ExternalUrls
|
externalUrls: ExternalUrls
|
||||||
) {
|
) {
|
||||||
final val device = LocalDevice(
|
val device = LocalDevice(
|
||||||
DeviceIdentity(uniqueSystemIdentifier("Nextcloud-DLNA-MediaServer"), ADVERTISEMENT_AGE_IN_S),
|
DeviceIdentity(uniqueSystemIdentifier("Nextcloud-DLNA-MediaServer"), ADVERTISEMENT_AGE_IN_S),
|
||||||
UDADeviceType(DEVICE_TYPE, VERSION),
|
UDADeviceType(DEVICE_TYPE, VERSION),
|
||||||
DeviceDetails(friendlyName, externalUrls.selfURI),
|
DeviceDetails(friendlyName, externalUrls.selfURI),
|
||||||
createDeviceIcon(),
|
createDeviceIcon(),
|
||||||
arrayOf(contentDirectoryService, connectionManagerService)
|
arrayOf(contentDirectoryService, connectionManagerService)
|
||||||
)
|
)
|
||||||
|
val serviceIdentifier: String get() = device.identity.udn.identifierString
|
||||||
|
|
||||||
init {
|
init {
|
||||||
logger.info("uniqueSystemIdentifier: {} ({})", device.identity.udn, friendlyName)
|
logger.info("uniqueSystemIdentifier: {} ({})", device.identity.udn, friendlyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object : KLogging() {
|
companion object : KLogging() {
|
||||||
const val ICON_FILENAME = "icon.png"
|
const val ICON_FILENAME = "icon.png"
|
||||||
private const val DEVICE_TYPE = "MediaServer"
|
private const val DEVICE_TYPE = "MediaServer"
|
||||||
|
|
|
@ -115,7 +115,7 @@ class ApacheStreamClient(
|
||||||
): Callable<StreamResponseMessage> {
|
): Callable<StreamResponseMessage> {
|
||||||
return Callable<StreamResponseMessage> {
|
return Callable<StreamResponseMessage> {
|
||||||
logger.trace("Sending HTTP request: $requestMessage")
|
logger.trace("Sending HTTP request: $requestMessage")
|
||||||
httpClient.execute<StreamResponseMessage>(request, createResponseHandler(requestMessage))
|
httpClient.execute(request, createResponseHandler())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ class ApacheStreamClient(
|
||||||
clientConnectionManager.shutdown()
|
clientConnectionManager.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createResponseHandler(requestMessage: StreamRequestMessage?): ResponseHandler<StreamResponseMessage> {
|
private fun createResponseHandler(): ResponseHandler<StreamResponseMessage> {
|
||||||
return ResponseHandler<StreamResponseMessage> { response ->
|
return ResponseHandler<StreamResponseMessage> { response ->
|
||||||
val statusLine = response.statusLine
|
val statusLine = response.statusLine
|
||||||
logger.trace("Received HTTP response: $statusLine")
|
logger.trace("Received HTTP response: $statusLine")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.schowek.nextclouddlna.nextcloud
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
import mu.KLogging
|
import mu.KLogging
|
||||||
|
import net.schowek.nextclouddlna.nextcloud.config.NextcloudConfigDiscovery
|
||||||
import net.schowek.nextclouddlna.nextcloud.content.ContentItem
|
import net.schowek.nextclouddlna.nextcloud.content.ContentItem
|
||||||
import net.schowek.nextclouddlna.nextcloud.content.ContentNode
|
import net.schowek.nextclouddlna.nextcloud.content.ContentNode
|
||||||
import net.schowek.nextclouddlna.nextcloud.content.MediaFormat
|
import net.schowek.nextclouddlna.nextcloud.content.MediaFormat
|
||||||
|
@ -96,9 +97,9 @@ class NextcloudDB(
|
||||||
private fun buildPath(f: Filecache): String {
|
private fun buildPath(f: Filecache): String {
|
||||||
return if (storageUsersMap.containsKey(f.storage)) {
|
return if (storageUsersMap.containsKey(f.storage)) {
|
||||||
val userName: String? = storageUsersMap[f.storage]
|
val userName: String? = storageUsersMap[f.storage]
|
||||||
"${nextcloudConfig.nextcloudDir}/$userName/${f.path}"
|
"${nextcloudConfig.nextcloudDir.absolutePath}/$userName/${f.path}"
|
||||||
} else {
|
} 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 mu.KLogging
|
||||||
import net.schowek.nextclouddlna.nextcloud.db.AppConfigRepository
|
import net.schowek.nextclouddlna.nextcloud.db.AppConfigRepository
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
|
||||||
import java.util.Arrays.*
|
import java.util.Arrays.*
|
||||||
import java.util.Objects.*
|
import java.util.Objects.*
|
||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class NextcloudConfigDiscovery(
|
class NextcloudConfigDiscovery(
|
||||||
@Value("\${nextcloud.filesDir}")
|
val appConfigRepository: AppConfigRepository,
|
||||||
val nextcloudDir: String,
|
val nextcloudDirProvider: NextcloudAppPathProvider
|
||||||
val appConfigRepository: AppConfigRepository
|
|
||||||
) {
|
) {
|
||||||
final val appDataDir: String = findAppDataDir()
|
val appDataDir: String = findAppDataDir()
|
||||||
final val supportsGroupFolders: Boolean = checkGroupFoldersSupport()
|
val nextcloudDir: File get() = nextcloudDirProvider.nextcloudDir
|
||||||
|
val supportsGroupFolders: Boolean = checkGroupFoldersSupport()
|
||||||
|
|
||||||
private fun checkGroupFoldersSupport(): Boolean {
|
private fun checkGroupFoldersSupport(): Boolean {
|
||||||
return "yes" == appConfigRepository.getValue("groupfolders", "enabled")
|
return "yes" == appConfigRepository.getValue("groupfolders", "enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findAppDataDir(): String {
|
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())
|
f.isDirectory && f.name.matches(APPDATA_NAME_PATTERN.toRegex())
|
||||||
})).findFirst().orElseThrow().name
|
})).findFirst().orElseThrow().name
|
||||||
.also {
|
.also {
|
|
@ -10,8 +10,8 @@ class ExternalUrls(
|
||||||
) {
|
) {
|
||||||
val selfUriString: String =
|
val selfUriString: String =
|
||||||
when (serverInfoProvider.port) {
|
when (serverInfoProvider.port) {
|
||||||
80 -> "http://${serverInfoProvider.address!!.hostAddress}"
|
80 -> "http://${serverInfoProvider.host}"
|
||||||
else -> "http://${serverInfoProvider.address!!.hostAddress}:${serverInfoProvider.port}"
|
else -> "http://${serverInfoProvider.host}:${serverInfoProvider.port}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,23 @@ package net.schowek.nextclouddlna.util
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
import mu.KLogging
|
import mu.KLogging
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.context.annotation.Profile
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.net.*
|
import java.net.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
interface ServerInfoProvider {
|
||||||
|
val host: String
|
||||||
|
val port: Int
|
||||||
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class ServerInfoProvider(
|
@Profile("!integration")
|
||||||
@param:Value("\${server.port}") val port: Int,
|
class ServerInfoProviderImpl(
|
||||||
|
@param:Value("\${server.port}") override val port: Int,
|
||||||
@param:Value("\${server.interface}") private val networkInterface: String
|
@param:Value("\${server.interface}") private val networkInterface: String
|
||||||
) {
|
) : ServerInfoProvider {
|
||||||
|
override val host: String get() = address!!.hostAddress
|
||||||
var address: InetAddress? = null
|
var address: InetAddress? = null
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
|
@ -22,9 +30,11 @@ class ServerInfoProvider(
|
||||||
|
|
||||||
private fun guessInetAddress(): InetAddress {
|
private fun guessInetAddress(): InetAddress {
|
||||||
return try {
|
return try {
|
||||||
val en0 = NetworkInterface.getByName(networkInterface).inetAddresses
|
val iface = NetworkInterface.getByName(networkInterface)
|
||||||
while (en0.hasMoreElements()) {
|
?: throw RuntimeException("Could not find network interface $networkInterface")
|
||||||
val x = en0.nextElement()
|
val addresses = iface.inetAddresses
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
val x = addresses.nextElement()
|
||||||
if (x is Inet4Address) {
|
if (x is Inet4Address) {
|
||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ server:
|
||||||
friendlyName: ${NEXTCLOUD_DLNA_FRIENDLY_NAME:Nextcloud-DLNA}
|
friendlyName: ${NEXTCLOUD_DLNA_FRIENDLY_NAME:Nextcloud-DLNA}
|
||||||
|
|
||||||
nextcloud:
|
nextcloud:
|
||||||
filesDir: ${NEXTCLOUD_DATA_DIR:/path/to/your/nextcloud/dir/ending/with/data}
|
filesDir: ${NEXTCLOUD_DATA_DIR}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
|
@ -15,3 +15,7 @@ spring:
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
mode: never
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,12 @@ package net.schowek.nextclouddlna.util
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class ExternalUrlsTest extends Specification {
|
class ExternalUrlsTest extends Specification {
|
||||||
def inetAddress = Mock(InetAddress)
|
|
||||||
def serverInfoProvider = Mock(ServerInfoProvider)
|
def serverInfoProvider = Mock(ServerInfoProvider)
|
||||||
|
|
||||||
def setup() {
|
|
||||||
serverInfoProvider.address >> inetAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
def "should generate main url for the service"() {
|
def "should generate main url for the service"() {
|
||||||
given:
|
given:
|
||||||
inetAddress.getHostAddress() >> host
|
|
||||||
serverInfoProvider.getPort() >> port
|
serverInfoProvider.getPort() >> port
|
||||||
|
serverInfoProvider.getHost() >> host
|
||||||
def sut = new ExternalUrls(serverInfoProvider)
|
def sut = new ExternalUrls(serverInfoProvider)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
@ -30,8 +25,8 @@ class ExternalUrlsTest extends Specification {
|
||||||
|
|
||||||
def "should generate content urls"() {
|
def "should generate content urls"() {
|
||||||
given:
|
given:
|
||||||
inetAddress.getHostAddress() >> host
|
|
||||||
serverInfoProvider.getPort() >> port
|
serverInfoProvider.getPort() >> port
|
||||||
|
serverInfoProvider.getHost() >> host
|
||||||
def sut = new ExternalUrls(serverInfoProvider)
|
def sut = new ExternalUrls(serverInfoProvider)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
|
Loading…
Reference in a new issue