설정 A 분야 12: 원격 서버 (RemoteSettings) - WebDAV · SFTP · SMB · FTP 목록 · 쉬는 시간 · WebDAV 외부 열기, 키 7
- 비밀 값 처리 (webdavServers 의 plainSecrets · 새 비밀 값은 평문으로 쓰지 않음) 그대로, 설명 주석도 함께 옮김 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
828d978812
commit
8d26d1af1e
|
|
@ -29,7 +29,7 @@ void main() {
|
|||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.ftpServers = [
|
||||
c.settings.remote.ftpServers = [
|
||||
const FtpServer(id: 'f1', name: 'NAS FTPS', host: '192.168.0.15', user: 'jj'),
|
||||
const FtpServer(id: 'f2', name: '옛 공유기', host: '192.168.0.1', security: 'none'),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ void main() {
|
|||
await shot('5_rejected', 'authorized_keys');
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
await settle(() => false, rounds: 10);
|
||||
expect(c.settings.sftpServers, isEmpty, reason: '[저장] 은 누르지 않음');
|
||||
expect(c.settings.remote.sftpServers, isEmpty, reason: '[저장] 은 누르지 않음');
|
||||
}, skip: skip, timeout: const Timeout(Duration(minutes: 3)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,15 +157,15 @@ class AppController extends ChangeNotifier {
|
|||
// 61: 기본 자막 언어 (MKV 기본 자막 트랙 · 플레이어가 먼저 켜는 자막)
|
||||
preferredSubtitleSetting = settings.preferredSubtitleLanguage;
|
||||
// WebDAV 서버 (파일 탐색기 · Rsync 화면 · 복사 · 동기화가 dav:// 경로로 쓴다)
|
||||
DavRegistry.configure(settings.webdavServers);
|
||||
DavRegistry.configure(settings.remote.webdavServers);
|
||||
// 88: SFTP 서버 (sftp:// 경로)
|
||||
SftpRegistry.configure(settings.sftpServers);
|
||||
SftpRegistry.configure(settings.remote.sftpServers);
|
||||
// 88: Android SMB 서버 (smb:// 경로)
|
||||
SmbRegistry.requireSecure = settings.smbRequireSecure;
|
||||
SmbRegistry.requireSecure = settings.remote.smbRequireSecure;
|
||||
// 관리자 판단 (10/20): 원격 서버 받기 · 올리기의 조각 사이 기다리기
|
||||
RemoteIdle.limit = Duration(seconds: settings.remoteIdleSeconds);
|
||||
SmbRegistry.configure(settings.smbServers);
|
||||
FtpRegistry.configure(settings.ftpServers);
|
||||
RemoteIdle.limit = Duration(seconds: settings.remote.idleSeconds);
|
||||
SmbRegistry.configure(settings.remote.smbServers);
|
||||
FtpRegistry.configure(settings.remote.ftpServers);
|
||||
outputRootOverride =
|
||||
(settings.mkvOutputRoot?.isNotEmpty ?? false) ? settings.mkvOutputRoot : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,10 @@ import '../core/reader_sources.dart' show defaultImageExtensions;
|
|||
import '../core/download_detect.dart';
|
||||
import '../core/encode_options.dart';
|
||||
import '../core/playlist.dart';
|
||||
import '../core/ftp.dart';
|
||||
import '../core/sftp.dart';
|
||||
import '../core/smb.dart';
|
||||
import '../core/webdav.dart';
|
||||
import '../services/app_shell.dart' show appIconOf;
|
||||
import '../core/remote_idle.dart';
|
||||
import 'settings/records.dart';
|
||||
import 'settings/settings_group.dart';
|
||||
import 'settings/remote_settings.dart';
|
||||
import 'settings/explorer_tool_settings.dart';
|
||||
import 'settings/vault_settings.dart';
|
||||
import 'settings/run_settings.dart';
|
||||
|
|
@ -30,6 +26,7 @@ import 'settings/explorer_settings.dart';
|
|||
export 'settings/records.dart';
|
||||
export 'settings/settings_group.dart';
|
||||
export 'settings/settings_store.dart';
|
||||
export 'settings/remote_settings.dart';
|
||||
export 'settings/explorer_tool_settings.dart';
|
||||
export 'settings/vault_settings.dart';
|
||||
export 'settings/run_settings.dart';
|
||||
|
|
@ -239,8 +236,6 @@ class AppSettings {
|
|||
/// 61: 기본 자막 언어 (MKV 의 기본 자막 트랙 · 플레이어가 먼저 켜는 자막). '' = 화면 언어 따르기, 그 밖은 자막 언어 코드 (ko · ja …)
|
||||
String preferredSubtitleLanguage = '';
|
||||
|
||||
/// 55: 로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때 'ask' (매번 묻기) · 'fetch' (늘 받아서) · 'url' (늘 주소로)
|
||||
String davExternalOpen = 'ask';
|
||||
|
||||
/// 39: 켤 때 "모든 파일 접근" 안내를 한 번 보였는지 (그 뒤로는 파일 탐색기 안의 안내로)
|
||||
bool allFilesHintShown = false;
|
||||
|
|
@ -249,25 +244,11 @@ class AppSettings {
|
|||
double subtitleScale = 1.0;
|
||||
|
||||
|
||||
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
|
||||
List<DavServer> webdavServers = [];
|
||||
|
||||
/// 88: SFTP 서버 (WebDAV 옆 탭). 비밀번호 · 개인 키 · 키 암호는 안전 저장소에만 (설정 파일에는 평문으로 쓰지 않는다)
|
||||
List<SftpServer> sftpServers = [];
|
||||
|
||||
/// 88: Android SMB 서버 (smbj). 비밀번호는 안전 저장소에만. Windows 는 운영체제의 \서버\공유 를 쓴다
|
||||
List<SmbServer> smbServers = [];
|
||||
|
||||
/// 보안 검토 6 (관리자 결정 10/18): SMB 서명이나 암호화가 되지 않는 연결은 막는다. 처음 끔 (오래된 NAS 가 붙지 않을 수 있어서).
|
||||
/// 꺼 두어도 서명 · 암호화가 안 되는 서버는 탐색기 탭에 ⚠
|
||||
bool smbRequireSecure = false;
|
||||
|
||||
/// l: FTP · FTPS 서버. 비밀번호는 안전 저장소에만 (설정 파일에는 평문으로 쓰지 않는다)
|
||||
List<FtpServer> ftpServers = [];
|
||||
|
||||
/// 관리자 판단 (10/20): 원격 서버 (FTP · SFTP · SMB · WebDAV) 받기 · 올리기에서 조각 사이를 기다리는 시간 (초).
|
||||
/// 이 시간 동안 아무것도 오가지 않으면 실패 ("서버가 답하지 않습니다"). 30 · 60 (처음) · 120 · 300 - 그 밖의 값은 60
|
||||
int remoteIdleSeconds = RemoteIdle.defaultSeconds;
|
||||
|
||||
// ───────── 123 · 121: AI 그림 · 해상도 올리기 ─────────
|
||||
|
||||
|
|
@ -352,8 +333,11 @@ class AppSettings {
|
|||
/// 탐색기 도구 - 시스템 폴더 (Shizuku) · 루트 · Wi-Fi 공유 · ZIP 묶기
|
||||
final tools = ExplorerToolSettings();
|
||||
|
||||
/// 원격 서버 (WebDAV · SFTP · SMB · FTP)
|
||||
final remote = RemoteSettings();
|
||||
|
||||
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools];
|
||||
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote];
|
||||
|
||||
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
|
||||
Map<String, Object?> toJson({bool plainSecrets = false}) {
|
||||
|
|
@ -406,19 +390,9 @@ class AppSettings {
|
|||
'recycleOnDelete': recycleOnDelete,
|
||||
'tripleTapAction': tripleTapAction,
|
||||
'preferredSubtitleLanguage': preferredSubtitleLanguage,
|
||||
'davExternalOpen': davExternalOpen,
|
||||
// 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더
|
||||
'allFilesHint2': allFilesHintShown,
|
||||
'subtitleScale': subtitleScale,
|
||||
'webdavServers': [
|
||||
for (final x in webdavServers) {...x.toJson(), if (plainSecrets) 'password': x.password},
|
||||
],
|
||||
// 새 비밀 값은 안전 저장소에 못 써도 평문으로 쓰지 않는다 (사용자 결정 54) - plainSecrets 여도 넣지 않음
|
||||
'sftpServers': [for (final x in sftpServers) x.toJson()],
|
||||
'smbServers': [for (final x in smbServers) x.toJson()],
|
||||
'smbRequireSecure': smbRequireSecure,
|
||||
'remoteIdleSeconds': remoteIdleSeconds,
|
||||
'ftpServers': [for (final x in ftpServers) x.toJson()],
|
||||
'playlistMode': playlistMode.name,
|
||||
'externalPlayers': externalPlayers,
|
||||
'encode': {
|
||||
|
|
@ -507,27 +481,8 @@ class AppSettings {
|
|||
..recycleOnDelete = j['recycleOnDelete'] != false
|
||||
..tripleTapAction = j['tripleTapAction'] == 'none' ? 'none' : 'move'
|
||||
..preferredSubtitleLanguage = j['preferredSubtitleLanguage'] is String ? j['preferredSubtitleLanguage'] as String : ''
|
||||
..davExternalOpen = const ['fetch', 'url'].contains(j['davExternalOpen']) ? j['davExternalOpen'] as String : 'ask'
|
||||
..allFilesHintShown = j['allFilesHint2'] == true
|
||||
..subtitleScale = ((j['subtitleScale'] as num?)?.toDouble() ?? 1.0).clamp(0.5, 2.5)
|
||||
..webdavServers = [
|
||||
for (final x in (j['webdavServers'] as List?) ?? const [])
|
||||
if (x is Map) DavServer.fromJson(x),
|
||||
]
|
||||
..sftpServers = [
|
||||
for (final x in (j['sftpServers'] as List?) ?? const [])
|
||||
if (x is Map) SftpServer.fromJson(x),
|
||||
]
|
||||
..smbRequireSecure = j['smbRequireSecure'] is bool ? j['smbRequireSecure'] as bool : false
|
||||
..remoteIdleSeconds = RemoteIdle.choices.contains(j['remoteIdleSeconds']) ? j['remoteIdleSeconds'] as int : RemoteIdle.defaultSeconds
|
||||
..smbServers = [
|
||||
for (final x in (j['smbServers'] as List?) ?? const [])
|
||||
if (x is Map) SmbServer.fromJson(x),
|
||||
]
|
||||
..ftpServers = [
|
||||
for (final x in (j['ftpServers'] as List?) ?? const [])
|
||||
if (x is Map) FtpServer.fromJson(x),
|
||||
]
|
||||
..playlistMode = pick(PlaylistMode.values, j['playlistMode'], PlaylistMode.series)
|
||||
..externalPlayers = ((j['externalPlayers'] as Map?) ?? const {}).cast<String, String>()
|
||||
..encode = EncodeSettings(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
import '../../core/webdav.dart';
|
||||
import '../../core/sftp.dart';
|
||||
import '../../core/smb.dart';
|
||||
import '../../core/ftp.dart';
|
||||
import '../../core/remote_idle.dart';
|
||||
import 'settings_group.dart';
|
||||
|
||||
/// 약속 (설정 A): 환경 설정 > 원격 서버 (WebDAV · SFTP · SMB · FTP) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
|
||||
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
|
||||
class RemoteSettings implements SettingsGroup {
|
||||
/// 55: 로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때 'ask' (매번 묻기) · 'fetch' (늘 받아서) · 'url' (늘 주소로)
|
||||
String davExternalOpen = 'ask';
|
||||
|
||||
/// WebDAV 서버 (파일 탐색기 · Rsync 화면 위쪽 "SD 카드" 옆 탭). 비밀번호는 이 설정 파일에만.
|
||||
List<DavServer> webdavServers = [];
|
||||
|
||||
/// 88: SFTP 서버 (WebDAV 옆 탭). 비밀번호 · 개인 키 · 키 암호는 안전 저장소에만 (설정 파일에는 평문으로 쓰지 않는다)
|
||||
List<SftpServer> sftpServers = [];
|
||||
|
||||
/// 88: Android SMB 서버 (smbj). 비밀번호는 안전 저장소에만. Windows 는 운영체제의 \서버\공유 를 쓴다
|
||||
List<SmbServer> smbServers = [];
|
||||
|
||||
/// 보안 검토 6 (관리자 결정 10/18): SMB 서명이나 암호화가 되지 않는 연결은 막는다. 처음 끔 (오래된 NAS 가 붙지 않을 수 있어서).
|
||||
/// 꺼 두어도 서명 · 암호화가 안 되는 서버는 탐색기 탭에 ⚠
|
||||
bool smbRequireSecure = false;
|
||||
|
||||
/// l: FTP · FTPS 서버. 비밀번호는 안전 저장소에만 (설정 파일에는 평문으로 쓰지 않는다)
|
||||
List<FtpServer> ftpServers = [];
|
||||
|
||||
/// 관리자 판단 (10/20): 원격 서버 (FTP · SFTP · SMB · WebDAV) 받기 · 올리기에서 조각 사이를 기다리는 시간 (초).
|
||||
/// 이 시간 동안 아무것도 오가지 않으면 실패 ("서버가 답하지 않습니다"). 30 · 60 (처음) · 120 · 300 - 그 밖의 값은 60
|
||||
int idleSeconds = RemoteIdle.defaultSeconds;
|
||||
|
||||
@override
|
||||
void write(Map<String, Object?> j, {required bool plainSecrets}) {
|
||||
j['webdavServers'] = [
|
||||
for (final x in webdavServers) {...x.toJson(), if (plainSecrets) 'password': x.password},
|
||||
];
|
||||
// 새 비밀 값은 안전 저장소에 못 써도 평문으로 쓰지 않는다 (사용자 결정 54) - plainSecrets 여도 넣지 않음
|
||||
j['sftpServers'] = [for (final x in sftpServers) x.toJson()];
|
||||
j['smbServers'] = [for (final x in smbServers) x.toJson()];
|
||||
j['smbRequireSecure'] = smbRequireSecure;
|
||||
j['ftpServers'] = [for (final x in ftpServers) x.toJson()];
|
||||
j['remoteIdleSeconds'] = idleSeconds;
|
||||
j['davExternalOpen'] = davExternalOpen;
|
||||
}
|
||||
|
||||
@override
|
||||
void read(Map<String, dynamic> j) {
|
||||
davExternalOpen = const ['fetch', 'url'].contains(j['davExternalOpen']) ? j['davExternalOpen'] as String : 'ask';
|
||||
webdavServers = [
|
||||
for (final x in (j['webdavServers'] as List?) ?? const [])
|
||||
if (x is Map) DavServer.fromJson(x),
|
||||
];
|
||||
sftpServers = [
|
||||
for (final x in (j['sftpServers'] as List?) ?? const [])
|
||||
if (x is Map) SftpServer.fromJson(x),
|
||||
];
|
||||
smbRequireSecure = j['smbRequireSecure'] is bool ? j['smbRequireSecure'] as bool : false;
|
||||
idleSeconds = RemoteIdle.choices.contains(j['remoteIdleSeconds']) ? j['remoteIdleSeconds'] as int : RemoteIdle.defaultSeconds;
|
||||
smbServers = [
|
||||
for (final x in (j['smbServers'] as List?) ?? const [])
|
||||
if (x is Map) SmbServer.fromJson(x),
|
||||
];
|
||||
ftpServers = [
|
||||
for (final x in (j['ftpServers'] as List?) ?? const [])
|
||||
if (x is Map) FtpServer.fromJson(x),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -65,21 +65,21 @@ class SettingsStore {
|
|||
static Map<String, String> _secretsOf(AppSettings s) => !s.rememberPasswords ? const {} : {
|
||||
for (final e in _osKeys.entries)
|
||||
if (_os(s, e.key).isNotEmpty) e.value: _os(s, e.key),
|
||||
for (final x in s.webdavServers)
|
||||
for (final x in s.remote.webdavServers)
|
||||
if (x.password.isNotEmpty) _davKey(x.id): x.password,
|
||||
for (final x in s.aiImage.services)
|
||||
if (x.apiKey.isNotEmpty) _aiKey(x.id): x.apiKey,
|
||||
for (final x in s.sftpServers) ...{
|
||||
for (final x in s.remote.sftpServers) ...{
|
||||
if (x.password.isNotEmpty) _sftpPw(x.id): x.password,
|
||||
if (x.privateKey.isNotEmpty) _sftpKey(x.id): x.privateKey,
|
||||
if (x.keyPassphrase.isNotEmpty) _sftpKp(x.id): x.keyPassphrase,
|
||||
},
|
||||
for (final x in s.smbServers)
|
||||
for (final x in s.remote.smbServers)
|
||||
if (x.password.isNotEmpty) _smbPw(x.id): x.password,
|
||||
for (final x in s.ftpServers)
|
||||
for (final x in s.remote.ftpServers)
|
||||
if (x.password.isNotEmpty) _ftpPw(x.id): x.password,
|
||||
// 서버 목록 (비밀번호 없이): 예전 판이 설정 파일에서 서버 목록을 지워도 되살릴 수 있게 (40)
|
||||
if (s.webdavServers.isNotEmpty) _davServers: jsonEncode([for (final x in s.webdavServers) x.toJson()]),
|
||||
if (s.remote.webdavServers.isNotEmpty) _davServers: jsonEncode([for (final x in s.remote.webdavServers) x.toJson()]),
|
||||
};
|
||||
|
||||
/// 저장소에 못 넣은 예전 평문 값을 설정 JSON 에 그대로 둔다
|
||||
|
|
@ -112,26 +112,26 @@ class SettingsStore {
|
|||
..openSubtitlesKey = os['openSubtitlesKey']!
|
||||
..openSubtitlesUser = os['openSubtitlesUser']!
|
||||
..openSubtitlesPassword = os['openSubtitlesPassword']!;
|
||||
s.webdavServers = [
|
||||
for (final x in s.webdavServers)
|
||||
s.remote.webdavServers = [
|
||||
for (final x in s.remote.webdavServers)
|
||||
x.password.isNotEmpty ? x : x.copyWith(password: stored[_davKey(x.id)] ?? ''),
|
||||
];
|
||||
s.aiImage.services = [
|
||||
for (final x in s.aiImage.services) x.apiKey.isNotEmpty ? x : x.copyWith(apiKey: stored[_aiKey(x.id)] ?? ''),
|
||||
];
|
||||
s.sftpServers = [
|
||||
for (final x in s.sftpServers)
|
||||
s.remote.sftpServers = [
|
||||
for (final x in s.remote.sftpServers)
|
||||
x.copyWith(
|
||||
password: x.password.isNotEmpty ? x.password : stored[_sftpPw(x.id)] ?? '',
|
||||
privateKey: x.privateKey.isNotEmpty ? x.privateKey : stored[_sftpKey(x.id)] ?? '',
|
||||
keyPassphrase: x.keyPassphrase.isNotEmpty ? x.keyPassphrase : stored[_sftpKp(x.id)] ?? '',
|
||||
),
|
||||
];
|
||||
s.smbServers = [
|
||||
for (final x in s.smbServers) x.password.isNotEmpty ? x : x.copyWith(password: stored[_smbPw(x.id)] ?? ''),
|
||||
s.remote.smbServers = [
|
||||
for (final x in s.remote.smbServers) x.password.isNotEmpty ? x : x.copyWith(password: stored[_smbPw(x.id)] ?? ''),
|
||||
];
|
||||
s.ftpServers = [
|
||||
for (final x in s.ftpServers) x.password.isNotEmpty ? x : x.copyWith(password: stored[_ftpPw(x.id)] ?? ''),
|
||||
s.remote.ftpServers = [
|
||||
for (final x in s.remote.ftpServers) x.password.isNotEmpty ? x : x.copyWith(password: stored[_ftpPw(x.id)] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ class SettingsStore {
|
|||
// 예전 판이 서버 목록을 지운 설정 파일 (항목 자체가 없음) → 저장소의 목록으로
|
||||
if (!s.extra.containsKey('webdavServers') && stored[_davServers] != null) {
|
||||
try {
|
||||
s.webdavServers = [
|
||||
s.remote.webdavServers = [
|
||||
for (final x in jsonDecode(stored[_davServers]!) as List)
|
||||
if (x is Map) DavServer.fromJson(x),
|
||||
];
|
||||
|
|
@ -163,7 +163,7 @@ class SettingsStore {
|
|||
final v = _os(s, e.key);
|
||||
if (v.isNotEmpty) _filePlain[e.value] = v;
|
||||
}
|
||||
for (final x in s.webdavServers) {
|
||||
for (final x in s.remote.webdavServers) {
|
||||
if (x.password.isNotEmpty) _filePlain[_davKey(x.id)] = x.password;
|
||||
}
|
||||
for (final x in s.aiImage.services) {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ Future<void> openExternalPlayable(BuildContext context, AppController c, String
|
|||
return;
|
||||
}
|
||||
// 102: 환경 설정 (매번 묻기 · 늘 받아서 · 늘 주소로)
|
||||
final pick = switch (c.settings.davExternalOpen) {
|
||||
final pick = switch (c.settings.remote.davExternalOpen) {
|
||||
'fetch' => 'fetch',
|
||||
'url' => 'url',
|
||||
_ => await showDialog<String>(
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ class ExplorerController extends ChangeNotifier {
|
|||
/// 저장 장치 + 서버들 (WebDAV · SFTP · SMB · FTP · 시스템 폴더 · 루트): (경로, 이름)
|
||||
List<(String, String)> get volumes => [
|
||||
...local,
|
||||
for (final s in c.settings.webdavServers) ('$davScheme${s.id}/', s.label),
|
||||
for (final s in c.settings.sftpServers) ('$sftpScheme${s.id}/', s.label),
|
||||
for (final s in c.settings.smbServers) ('$smbScheme${s.id}/', s.label),
|
||||
for (final s in c.settings.remote.webdavServers) ('$davScheme${s.id}/', s.label),
|
||||
for (final s in c.settings.remote.sftpServers) ('$sftpScheme${s.id}/', s.label),
|
||||
for (final s in c.settings.remote.smbServers) ('$smbScheme${s.id}/', s.label),
|
||||
// l: 평문 FTP 는 이름 앞에 ⚠
|
||||
for (final s in c.settings.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'),
|
||||
for (final s in c.settings.remote.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'),
|
||||
if (c.settings.tools.shizukuEnabled && shizukuSupported()) ('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')),
|
||||
if (c.settings.tools.rootEnabled && rootSupported()) (rootHome, tr('루트 (읽기 전용)')),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class ExplorerPaneHeader extends StatelessWidget {
|
|||
|
||||
/// 202: 창 위 줄 끝 - 탭이 넘쳐도 늘 보이는 버튼 (WebDAV 서버 추가 · 경로 입력)
|
||||
List<Widget> _end(BuildContext context) => [
|
||||
if (ctl.c.settings.webdavServers.isEmpty)
|
||||
if (ctl.c.settings.remote.webdavServers.isEmpty)
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class FtpSettings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servers = c.settings.ftpServers;
|
||||
final servers = c.settings.remote.ftpServers;
|
||||
return Column(children: [
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.folder_shared_outlined),
|
||||
|
|
@ -81,7 +81,7 @@ Future<bool> deleteFtpServer(BuildContext context, AppController c, FtpServer s)
|
|||
ok: tr('지우기'),
|
||||
);
|
||||
if (!ok) return false;
|
||||
await c.updateSettings((x) => x.ftpServers = [for (final y in x.ftpServers) if (y.id != s.id) y]);
|
||||
await c.updateSettings((x) => x.remote.ftpServers = [for (final y in x.remote.ftpServers) if (y.id != s.id) y]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ Future<bool> askTrustFtpCert(BuildContext context, String serverLabel, FtpCertEx
|
|||
|
||||
/// 믿은 인증서를 설정에 저장 (탐색기에서 처음 연결할 때)
|
||||
Future<void> trustFtpCert(AppController c, String id, FtpCertException e) =>
|
||||
c.updateSettings((x) => x.ftpServers = [for (final y in x.ftpServers) y.id == id ? y.copyWith(certFingerprint: e.fingerprint) : y]);
|
||||
c.updateSettings((x) => x.remote.ftpServers = [for (final y in x.remote.ftpServers) y.id == id ? y.copyWith(certFingerprint: e.fingerprint) : y]);
|
||||
|
||||
/// FTP 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null)
|
||||
Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServer? old}) async {
|
||||
|
|
@ -312,9 +312,9 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
),
|
||||
);
|
||||
if (saved == null) return null;
|
||||
await c.updateSettings((x) => x.ftpServers = [
|
||||
for (final y in x.ftpServers) y.id == saved.id ? saved : y,
|
||||
if (!x.ftpServers.any((y) => y.id == saved.id)) saved,
|
||||
await c.updateSettings((x) => x.remote.ftpServers = [
|
||||
for (final y in x.remote.ftpServers) y.id == saved.id ? saved : y,
|
||||
if (!x.remote.ftpServers.any((y) => y.id == saved.id)) saved,
|
||||
]);
|
||||
return saved;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,11 +139,11 @@ class _MasterDialogState extends State<_MasterDialog> {
|
|||
|
||||
/// 저장된 실제 비밀번호 · API 키를 지운다 (서버 목록 · 아이디는 남김)
|
||||
Future<void> clearStoredSecrets(AppController c) => c.updateSettings((s) => s
|
||||
..webdavServers = [for (final x in s.webdavServers) x.copyWith(password: '')]
|
||||
..remote.webdavServers = [for (final x in s.remote.webdavServers) x.copyWith(password: '')]
|
||||
// 88: SFTP 의 비밀번호 · 개인 키 · 키 암호도 (믿은 호스트 키는 비밀이 아니라 남김)
|
||||
..sftpServers = [for (final x in s.sftpServers) x.copyWith(password: '', privateKey: '', keyPassphrase: '')]
|
||||
..smbServers = [for (final x in s.smbServers) x.copyWith(password: '')]
|
||||
..ftpServers = [for (final x in s.ftpServers) x.copyWith(password: '')]
|
||||
..remote.sftpServers = [for (final x in s.remote.sftpServers) x.copyWith(password: '', privateKey: '', keyPassphrase: '')]
|
||||
..remote.smbServers = [for (final x in s.remote.smbServers) x.copyWith(password: '')]
|
||||
..remote.ftpServers = [for (final x in s.remote.ftpServers) x.copyWith(password: '')]
|
||||
// AI 서비스 API 키도 (문구 "비밀번호 · API 키" 와 같게 - 예전에는 남았다, 관리자 결정 10/17)
|
||||
..aiImage.services = [for (final x in s.aiImage.services) x.copyWith(apiKey: '')]
|
||||
..openSubtitlesKey = ''
|
||||
|
|
|
|||
|
|
@ -335,12 +335,12 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
|||
title: Text(tr('원격 서버 응답 기다리기')),
|
||||
subtitle: Text(tr('WebDAV · SFTP · FTP · SMB 에서 받거나 올리는 중에 이 시간 동안 아무것도 오가지 않으면 실패로 알립니다 (서버가 멈춰도 계속 기다리지 않게)')),
|
||||
trailing: DropdownButton<int>(
|
||||
value: RemoteIdle.choices.contains(s.remoteIdleSeconds) ? s.remoteIdleSeconds : RemoteIdle.defaultSeconds,
|
||||
value: RemoteIdle.choices.contains(s.remote.idleSeconds) ? s.remote.idleSeconds : RemoteIdle.defaultSeconds,
|
||||
items: [
|
||||
for (final n in RemoteIdle.choices)
|
||||
DropdownMenuItem(value: n, child: Text(n == RemoteIdle.defaultSeconds ? trf('{0}초 (기본)', [n]) : trf('{0}초', [n]))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.remoteIdleSeconds = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.remote.idleSeconds = v!),
|
||||
),
|
||||
),
|
||||
ViewerSettings(c: c),
|
||||
|
|
|
|||
|
|
@ -206,13 +206,13 @@ class _PlaySettingsSectionState extends State<PlaySettingsSection> {
|
|||
title: Text(tr('로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때')),
|
||||
subtitle: Text(tr('다른 앱에는 아이디 · 비밀번호를 넘기지 않습니다. 받아서 열기는 어느 앱에서나 열리고, 주소로 열기는 아이디를 묻는 앱 (VLC 등) 만 열립니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.davExternalOpen,
|
||||
value: s.remote.davExternalOpen,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'ask', child: Text(tr('매번 묻기 (기본)'))),
|
||||
DropdownMenuItem(value: 'fetch', child: Text(tr('늘 받아서 열기'))),
|
||||
DropdownMenuItem(value: 'url', child: Text(tr('늘 주소로 열기'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.davExternalOpen = v!),
|
||||
onChanged: (v) => c.updateSettings((x) => x.remote.davExternalOpen = v!),
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class SftpSettings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servers = c.settings.sftpServers;
|
||||
final servers = c.settings.remote.sftpServers;
|
||||
return Column(children: [
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.vpn_key_outlined),
|
||||
|
|
@ -83,7 +83,7 @@ Future<bool> deleteSftpServer(BuildContext context, AppController c, SftpServer
|
|||
ok: tr('지우기'),
|
||||
);
|
||||
if (!ok) return false;
|
||||
await c.updateSettings((x) => x.sftpServers = [for (final y in x.sftpServers) if (y.id != s.id) y]);
|
||||
await c.updateSettings((x) => x.remote.sftpServers = [for (final y in x.remote.sftpServers) if (y.id != s.id) y]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -137,8 +137,8 @@ Future<bool> askTrustHostKey(BuildContext context, String serverLabel, SftpHostK
|
|||
|
||||
/// 믿은 서버 키를 설정에 저장 (탐색기에서 처음 연결할 때)
|
||||
Future<void> trustSftpHostKey(AppController c, String id, SftpHostKeyException e) =>
|
||||
c.updateSettings((x) => x.sftpServers = [
|
||||
for (final y in x.sftpServers) y.id == id ? y.copyWith(hostKeyType: e.type, hostKeyFingerprint: e.fingerprint) : y,
|
||||
c.updateSettings((x) => x.remote.sftpServers = [
|
||||
for (final y in x.remote.sftpServers) y.id == id ? y.copyWith(hostKeyType: e.type, hostKeyFingerprint: e.fingerprint) : y,
|
||||
]);
|
||||
|
||||
/// SFTP 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null)
|
||||
|
|
@ -364,9 +364,9 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
|
|||
),
|
||||
);
|
||||
if (saved == null) return null;
|
||||
await c.updateSettings((x) => x.sftpServers = [
|
||||
for (final y in x.sftpServers) y.id == saved.id ? saved : y,
|
||||
if (!x.sftpServers.any((y) => y.id == saved.id)) saved,
|
||||
await c.updateSettings((x) => x.remote.sftpServers = [
|
||||
for (final y in x.remote.sftpServers) y.id == saved.id ? saved : y,
|
||||
if (!x.remote.sftpServers.any((y) => y.id == saved.id)) saved,
|
||||
]);
|
||||
return saved;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class SmbSettings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servers = c.settings.smbServers;
|
||||
final servers = c.settings.remote.smbServers;
|
||||
return Column(children: [
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.dns_outlined),
|
||||
|
|
@ -40,8 +40,8 @@ class SmbSettings extends StatelessWidget {
|
|||
title: Text(tr('SMB 서명 · 암호화 필수')),
|
||||
subtitle: Text(tr('서명도 암호화도 되지 않는 연결은 열지 않습니다 (중간에서 내용을 보거나 바꾸지 못하게). '
|
||||
'오래된 NAS 는 붙지 않을 수 있습니다. 꺼 두어도 그런 서버는 탐색기 탭에 ⚠ 를 보입니다')),
|
||||
value: c.settings.smbRequireSecure,
|
||||
onChanged: (v) => c.updateSettings((x) => x.smbRequireSecure = v),
|
||||
value: c.settings.remote.smbRequireSecure,
|
||||
onChanged: (v) => c.updateSettings((x) => x.remote.smbRequireSecure = v),
|
||||
),
|
||||
),
|
||||
for (final s in servers)
|
||||
|
|
@ -91,7 +91,7 @@ Future<bool> deleteSmbServer(BuildContext context, AppController c, SmbServer s)
|
|||
ok: tr('지우기'),
|
||||
);
|
||||
if (!ok) return false;
|
||||
await c.updateSettings((x) => x.smbServers = [for (final y in x.smbServers) if (y.id != s.id) y]);
|
||||
await c.updateSettings((x) => x.remote.smbServers = [for (final y in x.remote.smbServers) if (y.id != s.id) y]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -275,9 +275,9 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
|
|||
),
|
||||
);
|
||||
if (saved == null) return null;
|
||||
await c.updateSettings((x) => x.smbServers = [
|
||||
for (final y in x.smbServers) y.id == saved.id ? saved : y,
|
||||
if (!x.smbServers.any((y) => y.id == saved.id)) saved,
|
||||
await c.updateSettings((x) => x.remote.smbServers = [
|
||||
for (final y in x.remote.smbServers) y.id == saved.id ? saved : y,
|
||||
if (!x.remote.smbServers.any((y) => y.id == saved.id)) saved,
|
||||
]);
|
||||
return saved;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class WebDavSettings extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final servers = c.settings.webdavServers;
|
||||
final servers = c.settings.remote.webdavServers;
|
||||
return Column(children: [
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.cloud_outlined),
|
||||
|
|
@ -78,7 +78,7 @@ Future<bool> deleteDavServer(BuildContext context, AppController c, DavServer s)
|
|||
ok: tr('지우기'),
|
||||
);
|
||||
if (!ok) return false;
|
||||
await c.updateSettings((x) => x.webdavServers = [for (final y in x.webdavServers) if (y.id != s.id) y]);
|
||||
await c.updateSettings((x) => x.remote.webdavServers = [for (final y in x.remote.webdavServers) if (y.id != s.id) y]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -244,9 +244,9 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
|
|||
),
|
||||
);
|
||||
if (saved == null) return null;
|
||||
await c.updateSettings((x) => x.webdavServers = [
|
||||
for (final y in x.webdavServers) y.id == saved.id ? saved : y,
|
||||
if (!x.webdavServers.any((y) => y.id == saved.id)) saved,
|
||||
await c.updateSettings((x) => x.remote.webdavServers = [
|
||||
for (final y in x.remote.webdavServers) y.id == saved.id ? saved : y,
|
||||
if (!x.remote.webdavServers.any((y) => y.id == saved.id)) saved,
|
||||
]);
|
||||
return saved;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,18 +128,18 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('46: WebDAV 서버 지우기는 확인 뒤', (tester) async {
|
||||
c.settings.webdavServers = [const DavServer(id: 'n', name: '집 NAS', url: 'https://nas')];
|
||||
c.settings.remote.webdavServers = [const DavServer(id: 'n', name: '집 NAS', url: 'https://nas')];
|
||||
await tester.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: WebDavSettings(c: c)))));
|
||||
await tester.tap(find.byTooltip('지우기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('WebDAV 서버 "집 NAS" 을(를) 지울까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('취소'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.webdavServers, hasLength(1));
|
||||
expect(c.settings.remote.webdavServers, hasLength(1));
|
||||
await tester.tap(find.byTooltip('지우기'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '지우기'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(c.settings.webdavServers, isEmpty);
|
||||
expect(c.settings.remote.webdavServers, isEmpty);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ void main() {
|
|||
], everyElement(isTrue));
|
||||
expect(s.cleanupPrechecked, ['work', 'download']);
|
||||
expect(s.tripleTapAction, 'move');
|
||||
expect(s.davExternalOpen, 'ask');
|
||||
expect(s.remote.davExternalOpen, 'ask');
|
||||
// 이미 있던 설정의 기본값도 결정대로
|
||||
expect([s.browser.pauseOnLeave, s.run.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.run.inBackground, s.swipeNav],
|
||||
everyElement(isTrue));
|
||||
|
|
@ -50,11 +50,11 @@ void main() {
|
|||
..recycleOnDelete = false
|
||||
..cleanupPrechecked = ['logs']
|
||||
..tripleTapAction = 'none'
|
||||
..davExternalOpen = 'url')
|
||||
..remote.davExternalOpen = 'url')
|
||||
.toJson());
|
||||
expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords,
|
||||
b.recycleOnDelete], everyElement(isFalse));
|
||||
expect([b.cleanupPrechecked, b.tripleTapAction, b.davExternalOpen], [['logs'], 'none', 'url']);
|
||||
expect([b.cleanupPrechecked, b.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']);
|
||||
});
|
||||
|
||||
test('안쪽 → 바깥 허용을 끄면 늘 막는다', () {
|
||||
|
|
@ -69,7 +69,7 @@ void main() {
|
|||
final secrets = MemorySecretStore();
|
||||
final st = SettingsStore(p.join(dir.path, 'settings.json'), secrets);
|
||||
final s = await st.load();
|
||||
s.webdavServers = [const DavServer(id: 'd', name: 'n', url: 'https://n', user: 'u', password: 'pw')];
|
||||
s.remote.webdavServers = [const DavServer(id: 'd', name: 'n', url: 'https://n', user: 'u', password: 'pw')];
|
||||
await st.save(s);
|
||||
expect(secrets.values['dav.pw.d'], 'pw');
|
||||
s.rememberPasswords = false;
|
||||
|
|
@ -82,7 +82,7 @@ void main() {
|
|||
testWidgets('로그인이 필요한 WebDAV 를 다른 앱으로 - "$mode" 로 정해 두면 묻지 않는다', (tester) async {
|
||||
final shell = _Shell();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
|
||||
c.settings.davExternalOpen = mode;
|
||||
c.settings.remote.davExternalOpen = mode;
|
||||
DavRegistry.configure([const DavServer(id: 'n', name: 'n', url: 'http://127.0.0.1:9', user: 'u', password: 'p')]);
|
||||
addTearDown(() => DavRegistry.configure([]));
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: SizedBox())));
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ void main() {
|
|||
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '저장')));
|
||||
// 저장하면 설정에 남고 오른쪽 창이 그 서버로
|
||||
await settle(tester, () => find.text('원격').evaluate().isNotEmpty);
|
||||
expect(c.settings.webdavServers.single.url, server.url);
|
||||
final id = c.settings.webdavServers.single.id;
|
||||
expect(c.settings.remote.webdavServers.single.url, server.url);
|
||||
final id = c.settings.remote.webdavServers.single.id;
|
||||
expect(find.text('집 NAS'), findsNWidgets(3)); // 두 창의 탭 + 오른쪽 창의 맨 위 줄
|
||||
expect(find.text('WebDAV'), findsNothing);
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ void main() {
|
|||
testWidgets('178: 설정 옵션에 --remove-source-files 가 있으면 체크 켬 · [이동] · 경고, 끄면 정말 원본을 남긴다', (tester) async {
|
||||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..remote.webdavServers = [s]
|
||||
..copy.rsyncOptions = '-av --remove-source-files'
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
|
|
@ -164,7 +164,7 @@ void main() {
|
|||
};
|
||||
addTearDown(() => SecretGate.check = null);
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..remote.webdavServers = [s]
|
||||
..explorer.paths = [left, left]
|
||||
..folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')];
|
||||
DavRegistry.configure([s]);
|
||||
|
|
@ -192,7 +192,7 @@ void main() {
|
|||
testWidgets('Rsync 화면: 로컬 ↔ WebDAV 폴더 → (앱이 직접 맞춤 · rsync 필요 없음)', (tester) async {
|
||||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..remote.webdavServers = [s]
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
await pump(tester, ExplorerPage(device: device, c: c, rsync: true));
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ void main() {
|
|||
final secrets = MemorySecretStore();
|
||||
final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets);
|
||||
final s = AppSettings()
|
||||
..ftpServers = [const FtpServer(id: 'f1', name: 'NAS', host: 'nas.local', port: 2121, user: 'jj', password: 'ftp-secret', certFingerprint: 'SHA256:AB')];
|
||||
..remote.ftpServers = [const FtpServer(id: 'f1', name: 'NAS', host: 'nas.local', port: 2121, user: 'jj', password: 'ftp-secret', certFingerprint: 'SHA256:AB')];
|
||||
await store.save(s);
|
||||
expect(File(p.join(tmp.path, 'settings.json')).readAsStringSync(), isNot(contains('ftp-secret')));
|
||||
expect(secrets.values['ftp.pw.f1'], 'ftp-secret');
|
||||
final x = (await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load()).ftpServers.single;
|
||||
final x = (await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load()).remote.ftpServers.single;
|
||||
expect((x.host, x.port, x.user, x.password, x.security, x.certFingerprint), ('nas.local', 2121, 'jj', 'ftp-secret', 'explicit', 'SHA256:AB'));
|
||||
final stripped = AppSettings.stripSecrets({
|
||||
'ftpServers': [
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ void main() {
|
|||
AppController controller() {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webdavServers = [const DavServer(id: 'n', name: 'NAS', url: 'https://nas', user: 'u', password: 'pw')]
|
||||
..remote.webdavServers = [const DavServer(id: 'n', name: 'NAS', url: 'https://nas', user: 'u', password: 'pw')]
|
||||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesUser = 'ou'
|
||||
..openSubtitlesPassword = 'op';
|
||||
|
|
@ -238,7 +238,7 @@ void main() {
|
|||
await work(t);
|
||||
expect(result, isTrue);
|
||||
expect(lock.hasMaster, isFalse);
|
||||
expect(c.settings.webdavServers.single.password, 'pw', reason: '마스터만 골랐으니 비밀번호는 그대로');
|
||||
expect(c.settings.remote.webdavServers.single.password, 'pw', reason: '마스터만 골랐으니 비밀번호는 그대로');
|
||||
});
|
||||
|
||||
testWidgets('잊었습니다: 저장된 비밀번호 · API 키를 지우고 마스터 초기화 (서버 목록 · 아이디는 남김)', (t) async {
|
||||
|
|
@ -254,8 +254,8 @@ void main() {
|
|||
await t.tap(find.widgetWithText(FilledButton, '지우고 초기화'));
|
||||
await work(t);
|
||||
expect(find.text('초기화했습니다'), findsOneWidget);
|
||||
expect(c.settings.webdavServers.single.password, '');
|
||||
expect(c.settings.webdavServers.single.user, 'u');
|
||||
expect(c.settings.remote.webdavServers.single.password, '');
|
||||
expect(c.settings.remote.webdavServers.single.user, 'u');
|
||||
expect((c.settings.openSubtitlesKey, c.settings.openSubtitlesPassword, c.settings.openSubtitlesUser), ('', '', 'ou'));
|
||||
expect(lock.hasMaster, isFalse);
|
||||
});
|
||||
|
|
@ -285,8 +285,8 @@ void main() {
|
|||
await c.updateSettings((s) => s
|
||||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesPassword = 'op'
|
||||
..webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'PRIVATE KEY', keyPassphrase: 'kp')]
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..remote.sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'PRIVATE KEY', keyPassphrase: 'kp')]
|
||||
..aiImage.services = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')]);
|
||||
await store.save(c.settings);
|
||||
bool secret(String k) => k.startsWith('os.') || k.startsWith('dav.pw.') || k.startsWith('sftp.') || k.startsWith('ai.');
|
||||
|
|
@ -295,7 +295,7 @@ void main() {
|
|||
await store.save(c.settings);
|
||||
expect(secrets.values.keys.where(secret), isEmpty);
|
||||
// 목록 · 아이디 · 믿은 서버 키는 남김
|
||||
expect((c.settings.webdavServers.single.user, c.settings.sftpServers.single.user, c.settings.aiImage.services.single.url), ('u', 'u', 'https://ai'));
|
||||
expect((c.settings.remote.webdavServers.single.user, c.settings.remote.sftpServers.single.user, c.settings.aiImage.services.single.url), ('u', 'u', 'https://ai'));
|
||||
});
|
||||
|
||||
test('관리자 판단 (10/21): "앱 설정 전체" 는 이 판이 모르는 설정 키도 지우고, 다른 판의 비밀 값은 "저장된 비밀번호" 를 골랐을 때만', () async {
|
||||
|
|
@ -330,7 +330,7 @@ void main() {
|
|||
final c = controller()..settings.explorer.layout = 'single';
|
||||
await resetAllSettings(c);
|
||||
expect(c.settings.explorer.layout, AppSettings().explorer.layout);
|
||||
expect(c.settings.webdavServers, isEmpty);
|
||||
expect(c.settings.remote.webdavServers, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -134,14 +134,14 @@ void main() {
|
|||
tearDown(() => RemoteIdle.limit = const Duration(seconds: RemoteIdle.defaultSeconds));
|
||||
|
||||
test('설정 "원격 서버 응답 기다리기": 처음 60초 · 30 · 60 · 120 · 300 만 · 그 밖의 값은 60 · 오류 글은 초까지', () {
|
||||
expect(AppSettings().remoteIdleSeconds, 60);
|
||||
expect(AppSettings().remote.idleSeconds, 60);
|
||||
for (final n in RemoteIdle.choices) {
|
||||
expect(AppSettings.fromJson({'remoteIdleSeconds': n}).remoteIdleSeconds, n);
|
||||
expect(AppSettings.fromJson({'remoteIdleSeconds': n}).remote.idleSeconds, n);
|
||||
}
|
||||
for (final v in [0, 45, -1, 'x', null, 99999]) {
|
||||
expect(AppSettings.fromJson({'remoteIdleSeconds': v}).remoteIdleSeconds, 60, reason: '$v');
|
||||
expect(AppSettings.fromJson({'remoteIdleSeconds': v}).remote.idleSeconds, 60, reason: '$v');
|
||||
}
|
||||
expect((AppSettings()..remoteIdleSeconds = 120).toJson()['remoteIdleSeconds'], 120);
|
||||
expect((AppSettings()..remote.idleSeconds = 120).toJson()['remoteIdleSeconds'], 120);
|
||||
expect('${const RemoteIdleTimeout(60)}', '서버가 답하지 않습니다 (60초)');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ void main() {
|
|||
..copy.rsyncSource = 'custom'
|
||||
..copy.rsyncPath = rsync
|
||||
..copy.rsyncOptions = opts
|
||||
..webdavServers = [dav()];
|
||||
..remote.webdavServers = [dav()];
|
||||
for (final (label, target, local) in [('실시간 (로컬)', p.join(tmp.path, 'live'), true), ('실시간 (원격)', 'dav://$id/l', false)]) {
|
||||
final dir = local ? target : p.join(remote.path, 'l');
|
||||
writeScene(src, dir, srcFiles: _src, dstFiles: _dst);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ void main() {
|
|||
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
|
||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webdavServers = [s]
|
||||
..remote.webdavServers = [s]
|
||||
..explorer.layout = 'single'
|
||||
..copy.rsyncPaths = [left, 'dav://nas/'];
|
||||
DavRegistry.configure([s]);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ void main() {
|
|||
PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()),
|
||||
settingsStore: store);
|
||||
c.settings = await store.load();
|
||||
await c.updateSettings((s) => s.webdavServers = [
|
||||
await c.updateSettings((s) => s.remote.webdavServers = [
|
||||
const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw'),
|
||||
]);
|
||||
});
|
||||
|
|
@ -60,11 +60,11 @@ void main() {
|
|||
PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()),
|
||||
settingsStore: store);
|
||||
c.settings = await store.load();
|
||||
await c.updateSettings((s) => s.webdavServers = [
|
||||
await c.updateSettings((s) => s.remote.webdavServers = [
|
||||
const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'old'),
|
||||
]);
|
||||
secrets.failWrite = true;
|
||||
await c.updateSettings((s) => s.webdavServers = [s.webdavServers.single.copyWith(password: 'new')]);
|
||||
await c.updateSettings((s) => s.remote.webdavServers = [s.remote.webdavServers.single.copyWith(password: 'new')]);
|
||||
});
|
||||
expect(store.secretIssue.value?.revertsToOld, isTrue);
|
||||
final key = GlobalKey<ScaffoldMessengerState>();
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ void main() {
|
|||
final s = await store().load();
|
||||
expect(s.openSubtitlesKey, 'KEY-1');
|
||||
expect(s.openSubtitlesPassword, 'os-pw');
|
||||
expect(s.webdavServers.single.password, 'dav-pw');
|
||||
expect(s.remote.webdavServers.single.password, 'dav-pw');
|
||||
expect(secrets.values.values, containsAll(['KEY-1', 'me', 'os-pw', 'dav-pw']));
|
||||
for (final f in [file, '$file.bak']) {
|
||||
expect(text(f), isNot(contains('dav-pw')), reason: f);
|
||||
|
|
@ -50,7 +50,7 @@ void main() {
|
|||
expect(j['openSubtitlesSaved'], {'key': true, 'user': true, 'password': true});
|
||||
// 다시 켜도 그대로
|
||||
final again = await store().load();
|
||||
expect(again.webdavServers.single.password, 'dav-pw');
|
||||
expect(again.remote.webdavServers.single.password, 'dav-pw');
|
||||
expect(again.openSubtitlesKey, 'KEY-1');
|
||||
});
|
||||
|
||||
|
|
@ -59,32 +59,32 @@ void main() {
|
|||
await st.load();
|
||||
await st.save(AppSettings()
|
||||
..openSubtitlesKey = 'KEY-2'
|
||||
..webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw')]);
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw')]);
|
||||
// 예전 판: 모르는 항목을 지우고 저장
|
||||
File(file).writeAsStringSync(jsonEncode({'homeUrl': 'https://old.example'}));
|
||||
final s = await store().load();
|
||||
expect(s.browser.homeUrl, 'https://old.example');
|
||||
expect(s.webdavServers.single.url, 'https://nas');
|
||||
expect(s.webdavServers.single.password, 'pw');
|
||||
expect(s.remote.webdavServers.single.url, 'https://nas');
|
||||
expect(s.remote.webdavServers.single.password, 'pw');
|
||||
expect(s.openSubtitlesKey, 'KEY-2');
|
||||
// 사용자가 일부러 서버를 모두 지웠으면 (목록 항목은 있음) 되살리지 않는다
|
||||
final st2 = store();
|
||||
final s2 = await st2.load();
|
||||
await st2.save(s2..webdavServers = []);
|
||||
expect((await store().load()).webdavServers, isEmpty);
|
||||
await st2.save(s2..remote.webdavServers = []);
|
||||
expect((await store().load()).remote.webdavServers, isEmpty);
|
||||
});
|
||||
|
||||
test('비밀번호를 지우거나 서버를 지우면 안전 저장소에서도 지운다', () async {
|
||||
final st = store();
|
||||
final s = await st.load();
|
||||
s.webdavServers = [
|
||||
s.remote.webdavServers = [
|
||||
const DavServer(id: 'a', name: 'a', url: 'https://a', password: 'pa'),
|
||||
const DavServer(id: 'b', name: 'b', url: 'https://b', password: 'pb'),
|
||||
];
|
||||
s.openSubtitlesPassword = 'x';
|
||||
await st.save(s);
|
||||
expect(secrets.values.values, containsAll(['pa', 'pb', 'x']));
|
||||
s.webdavServers = [s.webdavServers.first];
|
||||
s.remote.webdavServers = [s.remote.webdavServers.first];
|
||||
s.openSubtitlesPassword = '';
|
||||
await st.save(s);
|
||||
expect(secrets.values.values, isNot(contains('pb')));
|
||||
|
|
@ -113,13 +113,13 @@ void main() {
|
|||
test('버전 보관본으로 되살려도 비밀번호는 안전 저장소의 것이 그대로', () async {
|
||||
final st = store();
|
||||
final s = await st.load();
|
||||
s.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw')];
|
||||
s.remote.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'pw')];
|
||||
await st.save(s);
|
||||
final snap = VersionSnapshot(dir.path);
|
||||
await snap.save('1.0');
|
||||
expect(text(p.join(snap.dirOf('1.0'), 'settings.json')), isNot(contains('"pw"')));
|
||||
await snap.restoreFrom(snap.dirOf('1.0'));
|
||||
expect((await store().load()).webdavServers.single.password, 'pw');
|
||||
expect((await store().load()).remote.webdavServers.single.password, 'pw');
|
||||
});
|
||||
|
||||
test('40-1: 안전 저장소에 쓰지 못하면 예전 평문은 파일에 그대로 · 새 값은 평문으로 쓰지 않음 · 알림 · [다시 시도]', () async {
|
||||
|
|
@ -136,9 +136,9 @@ void main() {
|
|||
if (File(f).existsSync()) expect(text(f), contains('old-pw'), reason: f);
|
||||
}
|
||||
// 다시 켜도 비밀번호가 남아 있다
|
||||
expect((await SettingsStore(file, secrets).load()).webdavServers.single.password, 'old-pw');
|
||||
expect((await SettingsStore(file, secrets).load()).remote.webdavServers.single.password, 'old-pw');
|
||||
// 새 비밀번호: 저장소에 못 쓰면 파일에 평문으로 쓰지 않는다 (예전 값은 그대로)
|
||||
s.webdavServers = [s.webdavServers.single.copyWith(password: 'new-pw')];
|
||||
s.remote.webdavServers = [s.remote.webdavServers.single.copyWith(password: 'new-pw')];
|
||||
await st.save(s);
|
||||
expect(text(file), isNot(contains('new-pw')));
|
||||
expect(text(file), contains('old-pw'));
|
||||
|
|
@ -160,7 +160,7 @@ void main() {
|
|||
final st = store();
|
||||
final s = await st.load();
|
||||
expect(st.secretIssue.value?.kind, SecretIssueKind.readFailed);
|
||||
s.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'typed')];
|
||||
s.remote.webdavServers = [const DavServer(id: 'd1', name: 'nas', url: 'https://nas', user: 'u', password: 'typed')];
|
||||
await st.save(s);
|
||||
expect(text(file), isNot(contains('typed')));
|
||||
expect(secrets.values, isEmpty);
|
||||
|
|
@ -196,7 +196,7 @@ void main() {
|
|||
expect(kept, isNot(contains(r'x\"y')));
|
||||
expect(kept, contains('"openSubtitlesKey": ""'));
|
||||
// .bak 의 값은 저장소로 옮기고 .bak 에서 지운다
|
||||
expect(s.webdavServers.single.password, 'bak-pw');
|
||||
expect(s.remote.webdavServers.single.password, 'bak-pw');
|
||||
expect(secrets.values['os.password'], 'bak-secret');
|
||||
expect(text('$file.bak'), isNot(contains('bak-pw')));
|
||||
expect(text('$file.bak'), isNot(contains('bak-secret')));
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ void main() {
|
|||
..values['sec.master'] = 'hash';
|
||||
final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets);
|
||||
final s = await store.load()
|
||||
..webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..sftpServers = [
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..remote.sftpServers = [
|
||||
const SftpServer(id: 's1', name: 'a', host: 'h', user: 'u', password: 'sp1', privateKey: 'KEY1', keyPassphrase: 'kp1'),
|
||||
const SftpServer(id: 's2', name: 'b', host: 'h', user: 'u', password: 'sp2'),
|
||||
];
|
||||
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
{for (final k in ['dav.pw.d1', 'sftp.pw.s1', 'sftp.key.s1', 'sftp.kp.s1', 'sftp.pw.s2']) k: secrets.values[k]},
|
||||
{'dav.pw.d1': 'dp', 'sftp.pw.s1': 'sp1', 'sftp.key.s1': 'KEY1', 'sftp.kp.s1': 'kp1', 'sftp.pw.s2': 'sp2'});
|
||||
// 서버 s1 을 지움: s1 의 아는 열쇠 (pw · key · kp) 만 없어지고, s2 · 모르는 꼴 · 마스터는 그대로
|
||||
s.sftpServers = [s.sftpServers.last];
|
||||
s.remote.sftpServers = [s.remote.sftpServers.last];
|
||||
await store.save(s);
|
||||
expect(secrets.values.keys.where((k) => k.endsWith('.s1')), ['sftp.cert.s1']);
|
||||
expect(secrets.values['sftp.pw.s2'], 'sp2');
|
||||
|
|
@ -74,10 +74,10 @@ void main() {
|
|||
..openSubtitlesKey = 'k'
|
||||
..openSubtitlesUser = 'u'
|
||||
..openSubtitlesPassword = 'op'
|
||||
..webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'KEY', keyPassphrase: 'kp')]
|
||||
..smbServers = [const SmbServer(id: 'm1', name: 'n', host: 'h', share: 'v', password: 'mp')]
|
||||
..ftpServers = [const FtpServer(id: 'f1', name: 'n', host: 'h', password: 'fp')]
|
||||
..remote.webdavServers = [const DavServer(id: 'd1', name: 'n', url: 'https://x', user: 'u', password: 'dp')]
|
||||
..remote.sftpServers = [const SftpServer(id: 's1', name: 'n', host: 'h', user: 'u', password: 'sp', privateKey: 'KEY', keyPassphrase: 'kp')]
|
||||
..remote.smbServers = [const SmbServer(id: 'm1', name: 'n', host: 'h', share: 'v', password: 'mp')]
|
||||
..remote.ftpServers = [const FtpServer(id: 'f1', name: 'n', host: 'h', password: 'fp')]
|
||||
..aiImage.services = [const AiService(id: 'a1', name: 'n', url: 'https://ai', kind: 'openai', apiKey: 'ak')];
|
||||
await store.save(s);
|
||||
final made = secrets.values.keys.where((k) => !k.startsWith('sec.')).toList()..sort();
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
final local = Directory(p.join(tmp.path, 'local'))..createSync();
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
await tester.runAsync(() => c.updateSettings((x) => x
|
||||
..sftpServers = [SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: s!.port, user: 'jj', password: 'pw')]
|
||||
..remote.sftpServers = [SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: s!.port, user: 'jj', password: 'pw')]
|
||||
..explorer.paths = ['sftp://s1/', local.path]));
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -69,8 +69,8 @@ void main() {
|
|||
await tester.tap(find.text('믿고 연결'));
|
||||
await settle(tester, () => find.text('hello.txt').evaluate().isNotEmpty);
|
||||
expect(find.text('hello.txt'), findsOneWidget);
|
||||
expect(c.settings.sftpServers.single.hostKeyFingerprint, s.fingerprint);
|
||||
expect(c.settings.sftpServers.single.hostKeyType, 'ssh-ed25519');
|
||||
expect(c.settings.remote.sftpServers.single.hostKeyFingerprint, s.fingerprint);
|
||||
expect(c.settings.remote.sftpServers.single.hostKeyType, 'ssh-ed25519');
|
||||
// 화면 내리기 · 연결 닫기
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.runAsync(() async => SftpRegistry.configure(const []));
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ void main() {
|
|||
final secrets = MemorySecretStore();
|
||||
final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets);
|
||||
final s = AppSettings()
|
||||
..sftpServers = [
|
||||
..remote.sftpServers = [
|
||||
const SftpServer(
|
||||
id: 's1',
|
||||
name: 'NAS',
|
||||
|
|
@ -51,7 +51,7 @@ void main() {
|
|||
expect(secrets.values['sftp.kp.s1'], 'kp-secret');
|
||||
expect(secrets.values['sftp.key.s1'], contains('OPENSSH PRIVATE KEY'));
|
||||
final back = await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load();
|
||||
final x = back.sftpServers.single;
|
||||
final x = back.remote.sftpServers.single;
|
||||
expect((x.host, x.port, x.user, x.legacyRsa, x.hostKeyFingerprint), ('nas.local', 2222, 'jj', true, 'SHA256:abc'));
|
||||
expect((x.password, x.keyPassphrase), ('pw-secret', 'kp-secret'));
|
||||
expect(x.privateKey, contains('OPENSSH PRIVATE KEY'));
|
||||
|
|
|
|||
|
|
@ -196,10 +196,10 @@ void main() {
|
|||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final secrets = MemorySecretStore();
|
||||
final store = SettingsStore(p.join(tmp.path, 'settings.json'), secrets);
|
||||
await store.save(AppSettings()..smbServers = [server]);
|
||||
await store.save(AppSettings()..remote.smbServers = [server]);
|
||||
expect(File(p.join(tmp.path, 'settings.json')).readAsStringSync(), isNot(contains('smb-Pw-5Qh9kT4r')));
|
||||
expect(secrets.values['smb.pw.m1'], 'smb-Pw-5Qh9kT4r');
|
||||
final back = (await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load()).smbServers.single;
|
||||
final back = (await SettingsStore(p.join(tmp.path, 'settings.json'), secrets).load()).remote.smbServers.single;
|
||||
expect((back.host, back.share, back.user, back.password), ('nas', 'video', 'jj', 'smb-Pw-5Qh9kT4r'));
|
||||
});
|
||||
|
||||
|
|
@ -210,9 +210,9 @@ void main() {
|
|||
expect(parseSmbAddress('/storage/x'), isNull);
|
||||
});
|
||||
test('보안 검토 6: 서명 · 암호화가 안 된 연결은 탐색기 탭에 ⚠ · 설정 "필수" 는 다리에 넘기고 막힌 이유를 알림', () async {
|
||||
expect(AppSettings().smbRequireSecure, isFalse, reason: '처음 끔 (오래된 NAS)');
|
||||
expect(AppSettings.fromJson(const {'smbRequireSecure': true}).smbRequireSecure, isTrue);
|
||||
expect(AppSettings.fromJson(const {'smbRequireSecure': 'x'}).smbRequireSecure, isFalse);
|
||||
expect(AppSettings().remote.smbRequireSecure, isFalse, reason: '처음 끔 (오래된 NAS)');
|
||||
expect(AppSettings.fromJson(const {'smbRequireSecure': true}).remote.smbRequireSecure, isTrue);
|
||||
expect(AppSettings.fromJson(const {'smbRequireSecure': 'x'}).remote.smbRequireSecure, isFalse);
|
||||
SmbRegistry.configure(const [server]);
|
||||
expect(DavPath.parse('smb://m1/').mark, '🗄', reason: '연결 전에는 모름');
|
||||
fake
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ void main() {
|
|||
rf('mirror/old.txt', 'o');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
// 예전에 확인한 쌍은 처음 맞출 때 지금 옵션을 기억하며 설정을 저장한다 (서버 목록도 설정에서 다시 읽음) - 시험 서버를 설정에도
|
||||
c.settings.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||||
c.settings.remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||||
final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true);
|
||||
c.settings.copy.liveSyncPairs = [pair];
|
||||
final live = LiveSync(c);
|
||||
|
|
@ -327,7 +327,7 @@ void main() {
|
|||
final src = p.join(local.path, 'live_src'), want = p.join(local.path, 'live_rsync'), got = p.join(remote.path, 'live');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
// 지우기를 정하면 설정을 저장하며 서버 목록을 설정에서 다시 읽는다 - 시험 서버를 설정에도
|
||||
c.settings.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||||
c.settings.remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||||
final bad = <String>[];
|
||||
for (final opts in sceneOptions) {
|
||||
writeScene(src, want);
|
||||
|
|
@ -367,7 +367,7 @@ void main() {
|
|||
rf('rqd/only.tmp', 'o');
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]
|
||||
..remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]
|
||||
..copy.rsyncOptions = '-av --exclude=*.tmp'
|
||||
..copy.liveSyncPairs = [
|
||||
LiveSyncPair(p.join(local.path, 'rq'), 'dav://$id/rqd',
|
||||
|
|
|
|||
Loading…
Reference in New Issue