diff --git a/scripts/dpl-resource-downloads/README.md b/scripts/dpl-resource-downloads/README.md new file mode 100644 index 0000000..3c2eb7e --- /dev/null +++ b/scripts/dpl-resource-downloads/README.md @@ -0,0 +1,32 @@ +# DPL 客户端下载接入 resource + +范围:仅DPL实例的Stellar运行配置、下载选择页及四份客户端下载教程;不修改XB核心、HN或resource采集服务。依据:2026-10-28用户确认将DPL GitHub客户端下载改接resource,并明确执行。 + +resource安装包源为`https://downloads.gemcook.net`,固定下载路径由当前软件目录核对。接入直接使用公开固定下载地址,不新增目录API调用、前端密钥或定时同步。resource每日更新同一固定路径,DPL无需跟着版本修改URL。 + +Windows默认Clash Verge Rev x64,macOS保留Apple/Intel选择,Android默认FlClash ARM64;Linux跳转DPL主题内的静态选择页,提供x64/ARM64的DEB/RPM。选择页另提供Windows ARM64及Android ARMv7。iOS原官方商店链接保留;OpenClash未纳入resource,路由器继续原官方入口,不替换软件或伪造resource链接。 + +## 执行 + +本目录脚本是DPL专用配置迁移,不通过更新XB公共分支实施。先以Git把本任务脚本发布到目标服务器隔离工具目录,记录确切提交;经登记白色机在site-jp02执行: + +```sh +python3 migrate.py plan +python3 migrate.py apply +``` + +脚本只允许已登记DPL本地根和生产根;修改前对所有用到的resource安装包核对当前目录、大小、完整SHA256和真实HTTPS Range206字节。保留全部其他env字段及教程并行修改;发现目标文案不符或并发文件变化就停止。更改保留文件权限和属主,原文件备份至root-only `/root/codex-backups/dpl/resource-downloads-时间/`,回退指令: + +```sh +python3 migrate.py rollback --backup /root/codex-backups/dpl/本次实际备份目录 +``` + +回退前检查部署后摘要,避免覆盖后续修改;有冲突须人工合并。重复执行无变化时不重复写入。 + +本地使用`--root /Users/hellcat/Documents/GemCodes/xb --backup 本次指定备份目录`,不从本机请求线上resource。运行配置`stellar/public/env.js`与`storage/theme/Stellar/env.js`分别维护,不能把包含站点设置的整个env加入Git;本地迁移后仅同步本任务六个下载字段到dev配置。 + +## 验收 + +部署后经白色机在服务器请求DPL实际HTTPS的env、下载选择页及四份教程,验证返回内容与目标地址及对应架构;只下载resource的Range片段,校验完整哈希在服务器本地进行。未打开线上浏览器时明确报告“已完成服务器侧验收,未做线上页面验收”。选择页保持简单原生HTML及锚点,实际下载不经过DPL PHP代理。 + +本地源配置/教程、线上运行配置、工具Git发布、真实HTTP结果分别记录;不能因脚本已提交就认定DPL已经切换。 diff --git a/scripts/dpl-resource-downloads/migrate.py b/scripts/dpl-resource-downloads/migrate.py new file mode 100644 index 0000000..a963e25 --- /dev/null +++ b/scripts/dpl-resource-downloads/migrate.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""DPL-only theme download settings migration; never edits XB core or other sites.""" +import argparse,datetime,hashlib,html,json,os,re,shutil,subprocess,sys,tempfile,urllib.request +from pathlib import Path +BASE='https://downloads.gemcook.net/downloads/software/' +PAGE='/theme/Stellar/resource-downloads.html' +FILES={ + 'windows':('clash-verge-rev','clash-verge-rev-windows-x64.exe'), + 'macos_arm64':('clash-verge-rev','clash-verge-rev-macos-arm64.dmg'), + 'macos_x64':('clash-verge-rev','clash-verge-rev-macos-x64.dmg'), + 'android':('flclash','flclash-android-arm64.apk'), +} +CHOICES=[('windows','Windows ARM64','clash-verge-rev','clash-verge-rev-windows-arm64.exe'),('macos','macOS Apple 芯片','clash-verge-rev','clash-verge-rev-macos-arm64.dmg'),('macos','macOS Intel 芯片','clash-verge-rev','clash-verge-rev-macos-x64.dmg'),('android','Android ARM64(多数手机)','flclash','flclash-android-arm64.apk'),('android','Android ARMv7(旧款手机)','flclash','flclash-android-armv7.apk')]+[('linux',f'Linux {arch.upper()} · {ext.upper()}','clash-verge-rev',f'clash-verge-rev-linux-{arch}.{ext}') for arch in ['x64','arm64'] for ext in ['deb','rpm']] +def url(software,name):return BASE+software+'/'+name +def digest(b):return hashlib.sha256(b).hexdigest() +def prepare(root): + env=root/'storage/theme/Stellar/env.js';s=env.read_text() + pat=r'([\"\']?client_downloads[\"\']?\s*:\s*\{)(.*?)(\})' + match=re.search(pat,s,re.S);assert match,'Missing client_downloads' + block=match[2];before=block + settings={k:url(*v) for k,v in FILES.items()};settings.update(macos=PAGE+'#macos',linux=PAGE+'#linux') + for key,value in settings.items(): + block,n=re.subn(r'([\"\']?'+key+r'[\"\']?\s*:\s*)([\"\'])([^\"\']*)\2',lambda m:m[1]+m[2]+value+m[2],block) + assert n==1,'Missing or duplicated download field: '+key + # Verify unrelated download options and every other setting are byte-identical. + assert all(re.search(r'[\"\']?'+k+r'[\"\']?\s*:\s*[\"\']([^\"\']*)',before)[1]==re.search(r'[\"\']?'+k+r'[\"\']?\s*:\s*[\"\']([^\"\']*)',block)[1] for k in ['ios','router']) + replacement=s[:match.start(2)]+block+s[match.end(2):] + page='''
按设备架构选择安装包。下载链接固定,软件更新后无需更换地址。
''' + for group,label in [('windows','Windows'),('macos','macOS'),('android','Android'),('linux','Linux')]: + page+=f'Ubuntu / Debian / Deepin 选择 DEB,Fedora / RHEL 系选择 RPM。运行 uname -m:x86_64 选择 X64,aarch64 选择 ARM64。Clash Verge Rev 需要图形桌面。
armv7l 选 armhf 或 armhfp',';当前 Clash Verge Rev 安装包仅提供 x64 与 ARM64'),('2.5.7、x86_64','x64'),('Clash.Verge_2.5.7_amd64.deb','clash-verge-rev-linux-x64.deb'),('Clash.Verge-2.5.7-1.x86_64.rpm','clash-verge-rev-linux-x64.rpm')]
+ }
+ for suffix,pairs in edits.items():
+ p=root/'resources/views/tutorials'/f'{suffix}.blade.php';old=p.read_text();new=old
+ for a,b in pairs:
+ assert a in new or b in new,'Tutorial content changed; review required: '+suffix
+ new=new.replace(a,b)
+ changes[p]=new.encode()
+ return {p:b for p,b in changes.items() if not p.exists() or p.read_bytes()!=b}
+def verify_resources():
+ base=Path('/www/wwwroot/resource-software/app/storage/app/private/software/current');pairs=set(FILES.values())|{(s,n) for _,_,s,n in CHOICES}
+ for sw,name in sorted(pairs):
+ cat=json.loads((base/sw/'catalog.json').read_text());asset=next(a for a in cat['assets'] if a['filename']==name)
+ p=base/sw/name;assert p.is_file() and p.stat().st_size==asset['size']
+ h=hashlib.sha256()
+ with p.open('rb') as f:
+ for chunk in iter(lambda:f.read(1024*1024),b''):h.update(chunk)
+ assert h.hexdigest()==asset['sha256']
+ req=urllib.request.Request(url(sw,name),headers={'Range':'bytes=0-31','User-Agent':'DPL-Resource-Acceptance/1.0'})
+ with urllib.request.urlopen(req,timeout=30) as response:
+ assert response.status==206 and response.geturl()==url(sw,name)
+ with p.open('rb') as source:assert response.read()==source.read(32)
+ print('verified',sw,name,cat['version'])
+def apply(root,backup):
+ changes=prepare(root)
+ if not changes:print('No changes required');return
+ snapshots={p:p.read_bytes() if p.exists() else None for p in changes}
+ if root==Path('/www/wwwroot/dp-link.net'):verify_resources()
+ for p,b in snapshots.items():
+ assert (p.read_bytes() if p.exists() else None)==b,'Concurrent file change: '+str(p)
+ backup.mkdir(parents=True,exist_ok=False);os.chmod(backup,0o700);manifest=[]
+ for i,(p,b) in enumerate(changes.items()):
+ old=snapshots[p];entry={'path':str(p),'before':digest(old) if old is not None else None,'after':digest(b),'backup':str(i)+'.bak' if old is not None else None}
+ if old is not None:(backup/entry['backup']).write_bytes(old)
+ manifest.append(entry)
+ (backup/'manifest.json').write_text(json.dumps(manifest,indent=2))
+ try:
+ for p,b in changes.items():
+ assert (p.read_bytes() if p.exists() else None)==snapshots[p],'Concurrent file change'
+ stat=p.stat() if p.exists() else p.parent.stat();fd,tmp=tempfile.mkstemp(dir=p.parent,prefix='.resource-download-')
+ with os.fdopen(fd,'wb') as f:f.write(b)
+ os.chmod(tmp,stat.st_mode&0o777 if p.exists() else 0o644)
+ if os.geteuid()==0:os.chown(tmp,stat.st_uid,stat.st_gid)
+ os.replace(tmp,p)
+ assert not prepare(root)
+ except Exception:
+ rollback(backup);raise
+ print('Applied',len(changes),'files; backup',backup)
+def rollback(backup):
+ rows=json.loads((backup/'manifest.json').read_text())
+ for row in rows:
+ p=Path(row['path']);assert p.exists() and digest(p.read_bytes())==row['after'],'File changed since deployment; manual merge required'
+ for row in rows:
+ p=Path(row['path'])
+ if row['backup']:p.write_bytes((backup/row['backup']).read_bytes())
+ else:p.unlink()
+ print('Rollback completed')
+if __name__=='__main__':
+ parser=argparse.ArgumentParser();parser.add_argument('mode',choices=['plan','apply','rollback']);parser.add_argument('--root',default='/www/wwwroot/dp-link.net');parser.add_argument('--backup');args=parser.parse_args();root=Path(args.root).resolve()
+ assert root in [Path('/www/wwwroot/dp-link.net'),Path('/Users/hellcat/Documents/GemCodes/xb')],'DPL roots only'
+ if args.mode=='rollback':rollback(Path(args.backup))
+ elif args.mode=='plan':print('\n'.join(str(p) for p in prepare(root)))
+ else:apply(root,Path(args.backup) if args.backup else Path('/root/codex-backups/dpl/resource-downloads-'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')))