Skip to content

Commit 35e9dab

Browse files
mengyushensimonxanderehung8383alwayschoco
authored
feat: Add Synology Photos Importer extensions (#1461)
The Synology Photos application is a useful tool for photo and album management, designed to operate on the DiskStation Manager (DSM) of Synology NAS devices. Due to most NAS systems being behind NAT and not accessible publicly, Synology offers a cloud service that acts as an intermediary. This service facilitates the transfer of data from various DTP extensions to the Synology cloud, from which users can subsequently download their data to their NAS. This PR includes: - Synology Photos Importer - Synology Videos Importer - Synology Media Importer --------- Co-authored-by: simonyeh <simonyeh@synology.com> Co-authored-by: ehung <ehung@synology.com> Co-authored-by: amychen <amychen@synology.com>
1 parent d60f719 commit 35e9dab

41 files changed

Lines changed: 3425 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

distributions/demo-server/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ dependencies {
8282
compile project(':extensions:auth:portability-auth-twitter')
8383
compile project(':extensions:auth:portability-auth-imgur')
8484
compile project(':extensions:auth:portability-auth-koofr')
85+
compile project(':extensions:auth:portability-auth-synology')
8586

8687
compile project(':extensions:data-transfer:portability-data-transfer-apple')
8788
compile project(':extensions:data-transfer:portability-data-transfer-daybook')
@@ -94,6 +95,7 @@ dependencies {
9495
compile project(':extensions:data-transfer:portability-data-transfer-rememberthemilk')
9596
compile project(':extensions:data-transfer:portability-data-transfer-smugmug')
9697
compile project(':extensions:data-transfer:portability-data-transfer-spotify')
98+
compile project(':extensions:data-transfer:portability-data-transfer-synology')
9799
compile project(':extensions:data-transfer:portability-data-transfer-twitter')
98100
compile project(':extensions:data-transfer:portability-data-transfer-imgur')
99101
compile project(':extensions:data-transfer:portability-data-transfer-koofr')

extensions/auth/portability-auth-google/src/main/java/org/datatransferproject/auth/google/GoogleOAuthConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.datatransferproject.types.common.models.DataVertical.CALENDAR;
2121
import static org.datatransferproject.types.common.models.DataVertical.CONTACTS;
2222
import static org.datatransferproject.types.common.models.DataVertical.MAIL;
23+
import static org.datatransferproject.types.common.models.DataVertical.MEDIA;
2324
import static org.datatransferproject.types.common.models.DataVertical.MUSIC;
2425
import static org.datatransferproject.types.common.models.DataVertical.PHOTOS;
2526
import static org.datatransferproject.types.common.models.DataVertical.SOCIAL_POSTS;
@@ -70,6 +71,7 @@ public Map<DataVertical, Set<String>> getExportScopes() {
7071
.put(SOCIAL_POSTS, ImmutableSet.of("https://www.googleapis.com/auth/plus.login"))
7172
.put(TASKS, ImmutableSet.of("https://www.googleapis.com/auth/tasks.readonly"))
7273
.put(VIDEOS, ImmutableSet.of("https://www.googleapis.com/auth/photoslibrary.readonly"))
74+
.put(MEDIA, ImmutableSet.of("https://www.googleapis.com/auth/photoslibrary.readonly"))
7375
.put(MUSIC, ImmutableSet.of("https://www.googleapis.com/auth/music"))
7476
.build();
7577
}
@@ -85,6 +87,7 @@ public Map<DataVertical, Set<String>> getImportScopes() {
8587
.put(PHOTOS, ImmutableSet.of("https://www.googleapis.com/auth/photoslibrary.appendonly"))
8688
.put(TASKS, ImmutableSet.of("https://www.googleapis.com/auth/tasks"))
8789
.put(VIDEOS, ImmutableSet.of("https://www.googleapis.com/auth/photoslibrary"))
90+
.put(MEDIA, ImmutableSet.of("https://www.googleapis.com/auth/photoslibrary"))
8891
.put(MUSIC, ImmutableSet.of("https://www.googleapis.com/auth/music"))
8992
.build();
9093
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id 'maven'
3+
id 'signing'
4+
}
5+
6+
dependencies {
7+
compile project(':portability-spi-api')
8+
compile project(':portability-spi-cloud')
9+
compile project(':libraries:auth')
10+
}
11+
12+
configurePublication(project)
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.datatransferproject.auth.synology;
2+
3+
import org.datatransferproject.auth.OAuth2ServiceExtension;
4+
5+
public class SynologyAuthServiceExtension extends OAuth2ServiceExtension {
6+
public SynologyAuthServiceExtension() {
7+
super(new SynologyOAuthConfig());
8+
}
9+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.datatransferproject.auth.synology;
2+
3+
import static org.datatransferproject.types.common.models.DataVertical.MEDIA;
4+
import static org.datatransferproject.types.common.models.DataVertical.PHOTOS;
5+
import static org.datatransferproject.types.common.models.DataVertical.VIDEOS;
6+
7+
import com.google.common.collect.ImmutableMap;
8+
import com.google.common.collect.ImmutableSet;
9+
import java.util.Map;
10+
import java.util.Set;
11+
import org.datatransferproject.auth.OAuth2Config;
12+
import org.datatransferproject.types.common.models.DataVertical;
13+
14+
public class SynologyOAuthConfig implements OAuth2Config {
15+
private static String AUTH_HOST = "https://identity.synology.com";
16+
private static String USER_INFO_SCOPE = "userinfo";
17+
private static String OFFLINE_ACCESS_SCOPE = "offline_access";
18+
19+
@Override
20+
public String getServiceName() {
21+
return "Synology";
22+
}
23+
24+
@Override
25+
public String getAuthUrl() {
26+
return AUTH_HOST + "/oauth2/auth";
27+
}
28+
29+
@Override
30+
public String getTokenUrl() {
31+
return AUTH_HOST + "/oauth2/token";
32+
}
33+
34+
@Override
35+
public Map<DataVertical, Set<String>> getExportScopes() {
36+
return ImmutableMap.<DataVertical, Set<String>>builder()
37+
.put(PHOTOS, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
38+
.put(VIDEOS, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
39+
.put(MEDIA, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
40+
.build();
41+
}
42+
43+
@Override
44+
public Map<DataVertical, Set<String>> getImportScopes() {
45+
return ImmutableMap.<DataVertical, Set<String>>builder()
46+
.put(PHOTOS, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
47+
.put(VIDEOS, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
48+
.put(MEDIA, ImmutableSet.of(USER_INFO_SCOPE, OFFLINE_ACCESS_SCOPE))
49+
.build();
50+
}
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package org.datatransferproject.auth.synology;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.datatransferproject.auth.synology.SynologyAuthServiceExtension
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Synology
2+
This folder contains the extension implementation for the
3+
[Synology](https://www.synology.com).
4+
5+
## Data Supported
6+
7+
Synology currently supports the following data types:
8+
- Photos
9+
- Videos
10+
11+
## Current State
12+
13+
- Photos/Videos:
14+
- Exporting is supported via Synology Photos or Bee Photos.
15+
- Importing is supported via the exporting source platforms (currently only Google Photos).
16+
17+
## Keys & Auth
18+
Synology uses OAuth 2 for authorization.
19+
20+
### Create an OAuth App
21+
Contact [Synology DTP Developer Team](dtp@synology.com) for further information.
22+
23+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025 The Data Transfer Project Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
plugins {
19+
id 'maven'
20+
id 'signing'
21+
}
22+
23+
dependencies {
24+
compile project(':portability-spi-transfer')
25+
compile project(':portability-spi-cloud')
26+
compile project(':portability-transfer')
27+
compile "com.squareup.okhttp3:okhttp:${okHttpVersion}"
28+
}
29+
30+
configurePublication(project)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.datatransferproject.datatransfer.synology;
2+
3+
import static org.datatransferproject.types.common.models.DataVertical.MEDIA;
4+
import static org.datatransferproject.types.common.models.DataVertical.PHOTOS;
5+
import static org.datatransferproject.types.common.models.DataVertical.VIDEOS;
6+
7+
import com.google.common.base.Preconditions;
8+
import com.google.common.collect.ImmutableList;
9+
import com.google.common.collect.ImmutableMap;
10+
import okhttp3.OkHttpClient;
11+
import org.datatransferproject.api.launcher.ExtensionContext;
12+
import org.datatransferproject.api.launcher.Monitor;
13+
import org.datatransferproject.datatransfer.synology.media.SynologyMediaImporter;
14+
import org.datatransferproject.datatransfer.synology.photos.SynologyPhotosImporter;
15+
import org.datatransferproject.datatransfer.synology.service.SynologyDTPService;
16+
import org.datatransferproject.datatransfer.synology.service.SynologyOAuthTokenManager;
17+
import org.datatransferproject.datatransfer.synology.uploader.SynologyUploader;
18+
import org.datatransferproject.datatransfer.synology.videos.SynologyVideosImporter;
19+
import org.datatransferproject.spi.cloud.storage.AppCredentialStore;
20+
import org.datatransferproject.spi.cloud.storage.JobStore;
21+
import org.datatransferproject.spi.transfer.extension.TransferExtension;
22+
import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor;
23+
import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutorExtension;
24+
import org.datatransferproject.spi.transfer.provider.Exporter;
25+
import org.datatransferproject.spi.transfer.provider.Importer;
26+
import org.datatransferproject.transfer.JobMetadata;
27+
import org.datatransferproject.types.common.models.DataVertical;
28+
import org.datatransferproject.types.transfer.auth.AppCredentials;
29+
import org.datatransferproject.types.transfer.serviceconfig.TransferServiceConfig;
30+
31+
public class SynologyTransferExtension implements TransferExtension {
32+
private static final ImmutableList<DataVertical> SUPPORTED_SERVICES =
33+
ImmutableList.of(PHOTOS, VIDEOS, MEDIA);
34+
private Monitor monitor;
35+
36+
private boolean initialized = false;
37+
38+
private ImmutableMap<DataVertical, Importer> importerMap;
39+
40+
@Override
41+
public String getServiceId() {
42+
return "Synology";
43+
}
44+
45+
private String getExtensionKey() {
46+
return getServiceId().toUpperCase() + "_KEY";
47+
}
48+
49+
private String getExtensionSecret() {
50+
return getServiceId().toUpperCase() + "_SECRET";
51+
}
52+
53+
@Override
54+
public Exporter<?, ?> getExporter(DataVertical transferDataType) {
55+
return null;
56+
}
57+
58+
@Override
59+
public Importer<?, ?> getImporter(DataVertical transferDataType) {
60+
Preconditions.checkArgument(initialized);
61+
Preconditions.checkArgument(SUPPORTED_SERVICES.contains(transferDataType));
62+
monitor.info(() -> "Getting importer for " + transferDataType);
63+
monitor.info(() -> "has importer: " + importerMap.containsKey(transferDataType));
64+
return importerMap.get(transferDataType);
65+
}
66+
67+
@Override
68+
public void initialize(ExtensionContext context) {
69+
if (initialized) {
70+
return;
71+
}
72+
73+
monitor = context.getMonitor();
74+
75+
AppCredentials appCredentials;
76+
try {
77+
appCredentials =
78+
context
79+
.getService(AppCredentialStore.class)
80+
.getAppCredentials(getExtensionKey(), getExtensionSecret());
81+
82+
} catch (Exception e) {
83+
monitor.info(
84+
() ->
85+
"Unable to retrieve Synology AppCredentials. Please configure Synology KEY and"
86+
+ " SECRET.");
87+
return;
88+
}
89+
90+
JobStore jobStore = context.getService(JobStore.class);
91+
IdempotentImportExecutor idempotentImportExecutor =
92+
context
93+
.getService(IdempotentImportExecutorExtension.class)
94+
.getRetryingIdempotentImportExecutor(context);
95+
SynologyOAuthTokenManager tokenManager = new SynologyOAuthTokenManager(appCredentials, monitor);
96+
OkHttpClient client = context.getService(OkHttpClient.class);
97+
TransferServiceConfig transferServiceConfig = context.getService(TransferServiceConfig.class);
98+
SynologyDTPService synologyDTPService =
99+
new SynologyDTPService(
100+
monitor,
101+
transferServiceConfig,
102+
JobMetadata.getExportService(),
103+
jobStore,
104+
tokenManager,
105+
client);
106+
SynologyUploader synologyUploader =
107+
new SynologyUploader(idempotentImportExecutor, monitor, synologyDTPService);
108+
109+
ImmutableMap.Builder<DataVertical, Importer> importerBuilder = ImmutableMap.builder();
110+
importerBuilder.put(MEDIA, new SynologyMediaImporter(monitor, tokenManager, synologyUploader));
111+
importerBuilder.put(
112+
PHOTOS, new SynologyPhotosImporter(monitor, tokenManager, synologyUploader));
113+
importerBuilder.put(
114+
VIDEOS, new SynologyVideosImporter(monitor, tokenManager, synologyUploader));
115+
importerMap = importerBuilder.build();
116+
117+
monitor.info(() -> "Initializing SynologyTransferExtension");
118+
initialized = true;
119+
}
120+
}

0 commit comments

Comments
 (0)