mirror of https://github.com/usbharu/Hideout.git
Merge pull request #188 from usbharu/feature/media-local-file-system
Feature/media local file system
This commit is contained in:
commit
e75abfa018
|
@ -42,3 +42,4 @@ out/
|
||||||
/tomcat/
|
/tomcat/
|
||||||
/tomcat-e2e/
|
/tomcat-e2e/
|
||||||
/e2eTest.log
|
/e2eTest.log
|
||||||
|
/files/
|
||||||
|
|
|
@ -8,13 +8,7 @@ hideout:
|
||||||
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
||||||
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
||||||
storage:
|
storage:
|
||||||
use-s3: true
|
type: local
|
||||||
endpoint: "http://localhost:8082/test-hideout"
|
|
||||||
public-url: "http://localhost:8082/test-hideout"
|
|
||||||
bucket: "test-hideout"
|
|
||||||
region: "auto"
|
|
||||||
access-key: ""
|
|
||||||
secret-key: ""
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
flyway:
|
flyway:
|
||||||
|
|
|
@ -8,13 +8,7 @@ hideout:
|
||||||
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
|
||||||
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
|
||||||
storage:
|
storage:
|
||||||
use-s3: true
|
type: local
|
||||||
endpoint: "http://localhost:8082/test-hideout"
|
|
||||||
public-url: "http://localhost:8082/test-hideout"
|
|
||||||
bucket: "test-hideout"
|
|
||||||
region: "auto"
|
|
||||||
access-key: ""
|
|
||||||
secret-key: ""
|
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
flyway:
|
flyway:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.usbharu.hideout.application.config
|
package dev.usbharu.hideout.application.config
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
|
||||||
|
@ -10,7 +11,8 @@ import java.net.URI
|
||||||
@Configuration
|
@Configuration
|
||||||
class AwsConfig {
|
class AwsConfig {
|
||||||
@Bean
|
@Bean
|
||||||
fun s3Client(awsConfig: StorageConfig): S3Client {
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
|
fun s3Client(awsConfig: S3StorageConfig): S3Client {
|
||||||
return S3Client.builder()
|
return S3Client.builder()
|
||||||
.endpointOverride(URI.create(awsConfig.endpoint))
|
.endpointOverride(URI.create(awsConfig.endpoint))
|
||||||
.region(Region.of(awsConfig.region))
|
.region(Region.of(awsConfig.region))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.usbharu.hideout.application.config
|
package dev.usbharu.hideout.application.config
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
@ -13,9 +14,6 @@ class SpringConfig {
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var config: ApplicationConfig
|
lateinit var config: ApplicationConfig
|
||||||
|
|
||||||
@Autowired
|
|
||||||
lateinit var storageConfig: StorageConfig
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun requestLoggingFilter(): CommonsRequestLoggingFilter {
|
fun requestLoggingFilter(): CommonsRequestLoggingFilter {
|
||||||
val loggingFilter = CommonsRequestLoggingFilter()
|
val loggingFilter = CommonsRequestLoggingFilter()
|
||||||
|
@ -33,9 +31,9 @@ data class ApplicationConfig(
|
||||||
val url: URL
|
val url: URL
|
||||||
)
|
)
|
||||||
|
|
||||||
@ConfigurationProperties("hideout.storage")
|
@ConfigurationProperties("hideout.storage.s3")
|
||||||
data class StorageConfig(
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
val useS3: Boolean,
|
data class S3StorageConfig(
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
val publicUrl: String,
|
val publicUrl: String,
|
||||||
val bucket: String,
|
val bucket: String,
|
||||||
|
@ -44,6 +42,20 @@ data class StorageConfig(
|
||||||
val secretKey: String
|
val secretKey: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メディアの保存にローカルファイルシステムを使用する際のコンフィグ
|
||||||
|
*
|
||||||
|
* @property path フォゾンする場所へのパス。 /から始めると絶対パスとなります。
|
||||||
|
* @property publicUrl 公開用URL 省略可能 指定するとHideoutがファイルを配信しなくなります。
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("hideout.storage.local")
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
|
||||||
|
data class LocalStorageConfig(
|
||||||
|
val path: String = "files",
|
||||||
|
val publicUrl: String?
|
||||||
|
)
|
||||||
|
|
||||||
@ConfigurationProperties("hideout.character-limit")
|
@ConfigurationProperties("hideout.character-limit")
|
||||||
data class CharacterLimit(
|
data class CharacterLimit(
|
||||||
val general: General = General(),
|
val general: General = General(),
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.application.config.LocalStorageConfig
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import kotlin.io.path.copyTo
|
||||||
|
import kotlin.io.path.createDirectories
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true)
|
||||||
|
/**
|
||||||
|
* ローカルファイルシステムにメディアを保存します
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* ApplicationConfigとLocalStorageConfigをもとに作成
|
||||||
|
*
|
||||||
|
* @param applicationConfig ApplicationConfig
|
||||||
|
* @param localStorageConfig LocalStorageConfig
|
||||||
|
*/
|
||||||
|
class LocalFileSystemMediaDataStore(
|
||||||
|
applicationConfig: ApplicationConfig, localStorageConfig: LocalStorageConfig
|
||||||
|
) : MediaDataStore {
|
||||||
|
|
||||||
|
private val savePath: Path = Path.of(localStorageConfig.path).toAbsolutePath()
|
||||||
|
|
||||||
|
private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/"
|
||||||
|
|
||||||
|
init {
|
||||||
|
savePath.createDirectories()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
|
||||||
|
val fileSavePath = buildSavePath(savePath, dataMediaSave.name)
|
||||||
|
val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataMediaSave.name)
|
||||||
|
|
||||||
|
|
||||||
|
dataMediaSave.thumbnailInputStream?.inputStream()?.use {
|
||||||
|
it.buffered().use { bufferedInputStream ->
|
||||||
|
thumbnailSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
|
||||||
|
.use { outputStream ->
|
||||||
|
outputStream.buffered().use {
|
||||||
|
bufferedInputStream.transferTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dataMediaSave.fileInputStream.inputStream().use {
|
||||||
|
it.buffered().use { bufferedInputStream ->
|
||||||
|
fileSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
|
||||||
|
.use { outputStream -> outputStream.buffered().use { bufferedInputStream.transferTo(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SuccessSavedMedia(
|
||||||
|
dataMediaSave.name, publicUrl + dataMediaSave.name, publicUrl + "thumbnail-" + dataMediaSave.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia {
|
||||||
|
logger.info("START Media upload. {}", dataSaveRequest.name)
|
||||||
|
val fileSavePath = buildSavePath(savePath, dataSaveRequest.name)
|
||||||
|
val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataSaveRequest.name)
|
||||||
|
|
||||||
|
val fileSavePathString = fileSavePath.toAbsolutePath().toString()
|
||||||
|
logger.info("MEDIA save. path: {}", fileSavePathString)
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSaveRequest.filePath.copyTo(fileSavePath)
|
||||||
|
dataSaveRequest.thumbnailPath?.copyTo(thumbnailSavePath)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("FAILED to Save the media.", e)
|
||||||
|
return FaildSavedMedia("FAILED to Save the media.", "Failed copy to path: $fileSavePathString", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
|
||||||
|
return SuccessSavedMedia(
|
||||||
|
dataSaveRequest.name, publicUrl + dataSaveRequest.name, publicUrl + "thumbnail-" + dataSaveRequest.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メディアを削除します。サムネイルも削除されます。
|
||||||
|
*
|
||||||
|
* @param id 削除するメディアのid [SuccessSavedMedia.name]を指定します。
|
||||||
|
*/
|
||||||
|
override suspend fun delete(id: String) {
|
||||||
|
logger.info("START Media delete. id: {}", id)
|
||||||
|
try {
|
||||||
|
buildSavePath(savePath, id).deleteIfExists()
|
||||||
|
buildSavePath(savePath, "thumbnail-$id").deleteIfExists()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("FAILED Media delete. id: {}", id, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSavePath(savePath: Path, name: String): Path = savePath.resolve(name)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(LocalFileSystemMediaDataStore::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,31 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メディアを保存するインタフェース
|
||||||
|
*
|
||||||
|
*/
|
||||||
interface MediaDataStore {
|
interface MediaDataStore {
|
||||||
|
/**
|
||||||
|
* InputStreamを使用してメディアを保存します
|
||||||
|
*
|
||||||
|
* @param dataMediaSave FileとThumbnailのinputStream
|
||||||
|
* @return 保存されたメディア
|
||||||
|
*/
|
||||||
suspend fun save(dataMediaSave: MediaSave): SavedMedia
|
suspend fun save(dataMediaSave: MediaSave): SavedMedia
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一時ファイルのパスを使用してメディアを保存します
|
||||||
|
*
|
||||||
|
* @param dataSaveRequest FileとThumbnailのパス
|
||||||
|
* @return 保存されたメディア
|
||||||
|
*/
|
||||||
suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia
|
suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia
|
||||||
|
|
||||||
|
/**
|
||||||
|
* メディアを削除します
|
||||||
|
* 実装はサムネイル、メタデータなども削除するべきです。
|
||||||
|
*
|
||||||
|
* @param id 削除するメディアのid 通常は[SuccessSavedMedia.name]を指定します。
|
||||||
|
*/
|
||||||
suspend fun delete(id: String)
|
suspend fun delete(id: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
package dev.usbharu.hideout.core.service.media
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.config.StorageConfig
|
import dev.usbharu.hideout.application.config.S3StorageConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import software.amazon.awssdk.core.sync.RequestBody
|
import software.amazon.awssdk.core.sync.RequestBody
|
||||||
import software.amazon.awssdk.services.s3.S3Client
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
@ -14,16 +15,17 @@ import software.amazon.awssdk.services.s3.model.GetUrlRequest
|
||||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig: StorageConfig) : MediaDataStore {
|
@ConditionalOnProperty("hideout.storage.type", havingValue = "s3")
|
||||||
|
class S3MediaDataStore(private val s3Client: S3Client, private val s3StorageConfig: S3StorageConfig) : MediaDataStore {
|
||||||
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
|
override suspend fun save(dataMediaSave: MediaSave): SavedMedia {
|
||||||
val fileUploadRequest = PutObjectRequest.builder()
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
.bucket(storageConfig.bucket)
|
.bucket(s3StorageConfig.bucket)
|
||||||
.key(dataMediaSave.name)
|
.key(dataMediaSave.name)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
|
val thumbnailKey = "thumbnail-${dataMediaSave.name}"
|
||||||
val thumbnailUploadRequest = PutObjectRequest.builder()
|
val thumbnailUploadRequest = PutObjectRequest.builder()
|
||||||
.bucket(storageConfig.bucket)
|
.bucket(s3StorageConfig.bucket)
|
||||||
.key(thumbnailKey)
|
.key(thumbnailKey)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -36,7 +38,7 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
|
RequestBody.fromBytes(dataMediaSave.thumbnailInputStream)
|
||||||
)
|
)
|
||||||
s3Client.utilities()
|
s3Client.utilities()
|
||||||
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build())
|
.getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(thumbnailKey).build())
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -44,14 +46,14 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
async {
|
async {
|
||||||
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
|
s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream))
|
||||||
s3Client.utilities()
|
s3Client.utilities()
|
||||||
.getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build())
|
.getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(dataMediaSave.name).build())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return SuccessSavedMedia(
|
return SuccessSavedMedia(
|
||||||
name = dataMediaSave.name,
|
name = dataMediaSave.name,
|
||||||
url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataMediaSave.name}",
|
url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataMediaSave.name}",
|
||||||
thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey"
|
thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,19 +61,19 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
logger.info("MEDIA upload. {}", dataSaveRequest.name)
|
logger.info("MEDIA upload. {}", dataSaveRequest.name)
|
||||||
|
|
||||||
val fileUploadRequest = PutObjectRequest.builder()
|
val fileUploadRequest = PutObjectRequest.builder()
|
||||||
.bucket(storageConfig.bucket)
|
.bucket(s3StorageConfig.bucket)
|
||||||
.key(dataSaveRequest.name)
|
.key(dataSaveRequest.name)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, dataSaveRequest.name)
|
logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, dataSaveRequest.name)
|
||||||
|
|
||||||
val thumbnailKey = "thumbnail-${dataSaveRequest.name}"
|
val thumbnailKey = "thumbnail-${dataSaveRequest.name}"
|
||||||
val thumbnailUploadRequest = PutObjectRequest.builder()
|
val thumbnailUploadRequest = PutObjectRequest.builder()
|
||||||
.bucket(storageConfig.bucket)
|
.bucket(s3StorageConfig.bucket)
|
||||||
.key(thumbnailKey)
|
.key(thumbnailKey)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, thumbnailKey)
|
logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, thumbnailKey)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
awaitAll(
|
awaitAll(
|
||||||
|
@ -92,8 +94,8 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
}
|
}
|
||||||
val successSavedMedia = SuccessSavedMedia(
|
val successSavedMedia = SuccessSavedMedia(
|
||||||
name = dataSaveRequest.name,
|
name = dataSaveRequest.name,
|
||||||
url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataSaveRequest.name}",
|
url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataSaveRequest.name}",
|
||||||
thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey"
|
thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
|
logger.info("SUCCESS Media upload. {}", dataSaveRequest.name)
|
||||||
|
@ -108,9 +110,9 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: String) {
|
override suspend fun delete(id: String) {
|
||||||
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build()
|
val fileDeleteRequest = DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key(id).build()
|
||||||
val thumbnailDeleteRequest =
|
val thumbnailDeleteRequest =
|
||||||
DeleteObjectRequest.builder().bucket(storageConfig.bucket).key("thumbnail-$id").build()
|
DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key("thumbnail-$id").build()
|
||||||
s3Client.deleteObject(fileDeleteRequest)
|
s3Client.deleteObject(fileDeleteRequest)
|
||||||
s3Client.deleteObject(thumbnailDeleteRequest)
|
s3Client.deleteObject(thumbnailDeleteRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
package dev.usbharu.hideout.core.service.media
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.application.config.LocalStorageConfig
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.io.path.readBytes
|
||||||
|
import kotlin.io.path.toPath
|
||||||
|
|
||||||
|
class LocalFileSystemMediaDataStoreTest {
|
||||||
|
|
||||||
|
private val path = String.javaClass.classLoader.getResource("400x400.png")?.toURI()?.toPath()!!
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save inputStreamを使用して正常に保存できる`() = runTest {
|
||||||
|
val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
||||||
|
val storageConfig = LocalStorageConfig("files", null)
|
||||||
|
|
||||||
|
val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig)
|
||||||
|
|
||||||
|
val fileInputStream = path.readBytes()
|
||||||
|
|
||||||
|
assertThat(fileInputStream.size).isNotEqualTo(0)
|
||||||
|
|
||||||
|
val mediaSave = MediaSave(
|
||||||
|
"test-media-1${UUID.randomUUID()}.png",
|
||||||
|
"",
|
||||||
|
fileInputStream,
|
||||||
|
fileInputStream
|
||||||
|
)
|
||||||
|
|
||||||
|
val save = localFileSystemMediaDataStore.save(mediaSave)
|
||||||
|
|
||||||
|
assertThat(save).isInstanceOf(SuccessSavedMedia::class.java)
|
||||||
|
|
||||||
|
save as SuccessSavedMedia
|
||||||
|
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve(save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun 一時ファイルを使用して正常に保存できる() = runTest {
|
||||||
|
val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
||||||
|
val storageConfig = LocalStorageConfig("files", null)
|
||||||
|
|
||||||
|
val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig)
|
||||||
|
|
||||||
|
val fileInputStream = path.readBytes()
|
||||||
|
|
||||||
|
assertThat(fileInputStream.size).isNotEqualTo(0)
|
||||||
|
|
||||||
|
val saveRequest = MediaSaveRequest(
|
||||||
|
"test-media-2${UUID.randomUUID()}.png",
|
||||||
|
"",
|
||||||
|
path,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
val save = localFileSystemMediaDataStore.save(saveRequest)
|
||||||
|
|
||||||
|
assertThat(save).isInstanceOf(SuccessSavedMedia::class.java)
|
||||||
|
|
||||||
|
save as SuccessSavedMedia
|
||||||
|
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve(save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun idを使用して削除できる() = runTest {
|
||||||
|
val applicationConfig = ApplicationConfig(URL("https://example.com"))
|
||||||
|
val storageConfig = LocalStorageConfig("files", null)
|
||||||
|
|
||||||
|
val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig)
|
||||||
|
|
||||||
|
val fileInputStream = path.readBytes()
|
||||||
|
|
||||||
|
assertThat(fileInputStream.size).isNotEqualTo(0)
|
||||||
|
|
||||||
|
val saveRequest = MediaSaveRequest(
|
||||||
|
"test-media-2${UUID.randomUUID()}.png",
|
||||||
|
"",
|
||||||
|
path,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
val save = localFileSystemMediaDataStore.save(saveRequest)
|
||||||
|
|
||||||
|
assertThat(save).isInstanceOf(SuccessSavedMedia::class.java)
|
||||||
|
|
||||||
|
save as SuccessSavedMedia
|
||||||
|
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve(save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name))
|
||||||
|
.exists()
|
||||||
|
.hasSize(fileInputStream.size.toLong())
|
||||||
|
|
||||||
|
|
||||||
|
localFileSystemMediaDataStore.delete(save.name)
|
||||||
|
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve(save.name))
|
||||||
|
.doesNotExist()
|
||||||
|
assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name))
|
||||||
|
.doesNotExist()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue