2025-01-18 20:20:58
今天进行 时光本 macOS端 更新的时候,上传安装包到 App Store Connect 成功后,但是在 App Store Connect 一直无法看到相应的构架包,查看邮件后发现了一个警告邮件:Action needed: The uploaded build for 时光本-日记本·笔记本·记事本·备忘录 has one or more issues.
App Store Connect
Hello,
We noticed one or more issues with a recent delivery for the following app:
时光本-日记本·笔记本·记事本·备忘录 Version 1.2.9 Build 129 Please correct the following issues and upload a new binary to App Store Connect.
ITMS-91109: Invalid package contents - The package contains one or more files with the com.apple.quarantine extended file attribute, such as “com.gorpeln.xxx.pkg/Payload/GPNotesForMac.app/Contents/Resources/zh-Hans.lproj/Main.strings”. This attribute isn’t permitted in macOS apps distributed on TestFlight or the App Store. Please remove the attribute from all files within your app and upload again.
Apple Developer Relations
在网上查找这个错误提示表明,提交到 App Store 的 macOS 应用程序包中包含一个或多个带有com.apple.quarantine扩展文件属性的文件,比如com.gorpeln.xxx.pkg/Payload/GPNotesForMac.app/Contents/Resources/zh-Hans.lproj/Main.strings,而这种属性是不被允许的。
com.apple.quarantine属性是 macOS 的一个安全特性,用于标记从不受信任的来源获取的文件,例如通过互联网下载或 AirDrop 接收的文件,以防止潜在的恶意软件。要解决这个问题,需要移除应用程序中所有文件的 com.apple.quarantine 属性。
看到网上的结果后知道是安装包中某些文件被添加了com.apple.quarantine 属性,具体可能导致相关错误的原因也很多:
但是具体怎么被添加的就不得而知了,于是就进行了漫长的排查解决过程,最后结果让自己很无奈。
首先导出安装包,按照提示路径,找到对应文件,使用 xattr 查找该文件是否有 com.apple.quarantine ,查找结果显示的确有,并且有标记是keka解压软件,因为我是用的keka进行的安装包解压,想着keka的标记应该是该流程添加,并不是原来 com.apple.quarantine 属性添加的直接原因,于是就尝试其他方法解决该问题。
为什么要更新 Xcode 呢,因为在提交 macOS 版本前先提交了 iOS 版本,iOS 版本提交后,提交成功页面显示有警告内容,提示马上要强制使用 Xcode 16 进行开发发布了,所以就想着是不是当前 Xcode 版本太低了导致的呢,于是就进行了更新操作,经过漫长的下载安装,问题依然存在,使用新的 Mac 用户也不行,只能想其他方法了。
Xcode 更新不行,想着提示有具体的安装包错误内容路径,就操作删除相关内容的 com.apple.quarantine 属性,网上也有相应的操作流程xattr -rd com.apple.quarantine /path/to/YourApp.app
,操作后继续提交,还是不行,还是同样的错误。
分析这个步骤只是进行简单的安装包操作,没有对安装包里面的内容遍历操作,于是想着进行一下遍历删除操作,但是该操作又无法直接遍历安装包内部内容,于是就进行相应的解压,遍历删除相关属性,操作还算顺利,并且重新打包上传,上传直接就报错了,提示证书问题,确实重新打包没有使用证书,重新查找资料,配置相关重新打包证书,完成打包成功上传,但是苹果接着发来邮件提示证书还是有问题。
Hello,
We noticed one or more issues with a recent delivery for the following app:
时光本-日记本·笔记本·记事本·备忘录 Version 1.3.4 Build 134 Please correct the following issues and upload a new binary to App Store Connect.
ITMS-90237: The product archive package’s signature is invalid. Ensure that it is signed with your ‘3rd Party Mac Developer Installer’ certificate.
Apple Developer Relations
查找 ITMS-90237 问题的解决方法,正确配置相关证书后依然不行,感觉这个方法不太靠谱,虽然能够成功删除 com.apple.quarantine 属性,但是重新将解压的安装包打包十分复杂,想着还是换个方法。
该步骤相关操作代码:
import os
import subprocess
import shutil
def remove_quarantine_attribute(pkg_path):
extract_dir = os.path.expanduser('~/Desktop/unpacked_pkg')
if os.path.exists(extract_dir):
try:
shutil.rmtree(extract_dir)
print(f"已删除现有的 {extract_dir} 目录。")
except Exception as e:
print(f"删除 {extract_dir} 目录时出错: {e}")
return
try:
os.makedirs(extract_dir)
print(f"成功创建 {extract_dir} 目录。")
except Exception as e:
print(f"创建 {extract_dir} 目录时出错: {e}")
return
try:
# 尝试使用 xar 解压
print("尝试使用 xar 解压 pkg 文件...")
result = subprocess.run(['xar', '-xf', pkg_path, '-C', extract_dir], capture_output=True, text=True)
if result.returncode != 0:
print(f"使用 xar 解压失败: {result.stderr}")
return
else:
print("使用 xar 解压成功。")
# 打印解压后的目录内容,便于检查
print(f"解压后的目录 {extract_dir} 内容:")
for item in os.listdir(extract_dir):
print(item)
pkg_inner_dir = os.path.join(extract_dir, 'com.gorpeln.xxx.pkg')
payload_path = os.path.join(pkg_inner_dir, 'Payload')
if os.path.exists(pkg_inner_dir) and os.path.exists(payload_path):
# 进入 Payload 所在目录并进行进一步处理
os.chdir(pkg_inner_dir)
try:
# 执行 cat Payload | gunzip -dc | cpio -i 操作
print("正在对 Payload 进行进一步解压...")
command = 'cat Payload | gunzip -dc | cpio -i'
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
print("Payload 进一步解压成功。")
else:
print(f"对 Payload 进一步解压时出错: {result.stderr}")
except subprocess.TimeoutExpired:
print("对 Payload 进一步解压时超时,请检查 Payload 文件是否正常。")
except subprocess.CalledProcessError as e:
print(f"对 Payload 进一步解压时出错: {e.stderr}")
finally:
# 切换回上级目录
os.chdir('..')
# 遍历所有文件并移除属性
for root, dirs, files in os.walk(extract_dir):
for file in files:
file_path = os.path.join(root, file)
try:
subprocess.run(['xattr', '-d', 'com.apple.quarantine', file_path], check=True)
print(f"成功移除 {file_path} 的 com.apple.quarantine 属性。")
except subprocess.CalledProcessError:
# 如果文件没有该属性,忽略错误
pass
# 找到 .app 文件
app_path = None
payload_extracted_dir = os.path.join(pkg_inner_dir)
for item in os.listdir(payload_extracted_dir):
if item.endswith('.app'):
app_path = os.path.join(payload_extracted_dir, item)
break
if app_path is None:
print("未找到有效的 .app 应用程序包。")
return
# 签名 .app 包内的所有文件
certificate_name_app = "3rd Party Mac Developer Application: xxxxxxxx (xxxxxxxxx)"
find_command = f'find "{app_path}" -type f -exec codesign --force --deep --sign "{certificate_name_app}" \{\{\}} \;'
subprocess.run(find_command, shell=True)
# 签名 .app 包本身
codesign_command = f'codesign --force --deep --sign "{certificate_name_app}" "{app_path}"'
subprocess.run(codesign_command, shell=True)
# 重新打包成 pkg 文件
new_pkg_path = pkg_path.replace('.pkg', '_cleaned.pkg')
install_path = '/Applications'
certificate_name_installer = "3rd Party Mac Developer Installer: xxxxxxxx (xxxxxxxxx)"
productbuild_command = [
'productbuild',
'--component', app_path, install_path,
'--sign', certificate_name_installer,
new_pkg_path
]
result = subprocess.run(productbuild_command, capture_output=True, text=True)
if result.returncode != 0:
print(f"重新打包并签名 pkg 文件时出错:{result.stderr}")
else:
print(f"处理完成,新的签名 pkg 文件已生成: {new_pkg_path}")
except Exception as e:
print(f"处理过程中出现错误: {e}")
finally:
if os.path.exists(extract_dir):
try:
shutil.rmtree(extract_dir)
print(f"已成功删除临时目录 {extract_dir}。")
except Exception as e:
print(f"删除临时目录 {extract_dir} 时出错: {e}")
# 使用示例
pkg_path = '/Users/gorpeln/Desktop/123/GPNotesForMac.pkg'
remove_quarantine_attribute(pkg_path)
怀疑是电脑上安装的某些破解软件导致的,于是进行删除操作,包括删除了keka解压软件,再次尝试打包,问题依然没有解决。想想上个版本到现在电脑上也没有新安装什么应用,应该不是这个原因,只能想其他方法。
尝试直接删除提示有问题的文件,该文件在项目中是语言适配文件,做本地化处理的,删除后虽然对应用体验有一定的影响,但是也没有好的办法,只能先删了提交后再慢慢看具体问题了。删除后提交,居然又发邮件提错误了。。。
App Store Connect
Hello,
We noticed one or more issues with a recent delivery for the following app:
时光本-日记本·笔记本·记事本·备忘录 Version 1.3.8 Build 138 Please correct the following issues and upload a new binary to App Store Connect.
ITMS-91109: Invalid package contents - The package contains one or more files with the com.apple.quarantine extended file attribute, such as “com.gorpeln.xxx.pkg/Payload/GPNotesForMac.app/Contents/Resources/MLHudAlertInfo.png”. This attribute isn’t permitted in macOS apps distributed on TestFlight or the App Store. Please remove the attribute from all files within your app and upload again.
Apple Developer Relations
看到这个错误后心凉半截,删除了一个文件,但是同样错误又显示了一个新的文件,肯定不能再删除了,因为还不知道到底有多少文件被标记了 com.apple.quarantine 属性,其中被标记的还可能是核心代码,删除后影响项目运行的。并且标记的这个文件应该是一个第三方库文件,想着应该是第三方库有问题,输入搜索后没有发现对应文件,猜想可能是pod导入导致的错误。
查看 Podfile 文件中第三方库都没有固定版本,想着可能是第三方库更新到新版本后出现了相关问题,于是就设置了所有库的对应版本并进行更新,提交上传,问题依旧。迷茫了,不知道该怎么办了。
脑子一昏,想着是不是 Xcode 版本高的问题,用比较旧的版本会不会好呢,于是就下载安装了。显而易见,并不会。因为在app上个版本到现在我除了这次升级,并没有操作 Xcode 版本,正常不会是 Xcode 版本问题的。
不知不觉,这个问题搞了一天了。没想法了,想着还是直接删除提示的对应文件吧,再次搜索提示的MLHudAlertInfo.png,居然看到了这个相关文件,进入查看具体内容,查找在Finder中的内容,确实存在MLHudAlertInfo.png文件。那这就好办了呀,直接查看这个文件是否包含 com.apple.quarantine 属性就行了呀,使用 xattr 查看后,的确存在,我感觉有点无语,基本确认是源码问题,直接使用 xattr 遍历删除项目文件夹下所有文件,重新提交上传,问题完美解决了。
具体该问题是怎么导致的呢?
总结这次问题,为什么耗费了这么长时间呢?
最后,如果感兴趣可以去查看使用一下【时光本】应用,是一款专注效率与记录的笔记工具。可以帮助你整理各种信息,包括便签、清单、图片、纪念日、地址、链接、银行卡、名片、账号、密码等
2024-12-21 15:56:22
想为你的博客增添浓浓的春节氛围吗?快来了解我们的独特功能!设定时间后,两个喜庆的灯笼将准时在博客右上角亮起,灯笼上 “新春快乐” 的祝福熠熠生辉,瞬间让你的博客充满节日的欢乐与温馨。这不仅是装饰,更是为访客送上真挚祝福的创意方式,快来探索如何让你的博客在春节别具一格吧。
效果 方法
<link rel="stylesheet" type="text/css" href="../assets/css/deng.css">
注意:将文件下载到自己博客使用
https://gorpeln.top/assets/css/deng.css
html代码
<!-- 春节灯笼 -->
<div style="display:none;" id="lanterns">
<div class="deng-box">
<div class="deng">
<div class="xian"></div>
<div class="deng-a">
<div class="deng-b">
<div class="deng-t">快乐</div>
</div>
</div>
<div class="shui shui-a">
<div class="shui-c"></div>
<div class="shui-b"></div>
</div>
</div>
</div>
<div class="deng-box1">
<div class="deng">
<div class="xian"></div>
<div class="deng-a">
<div class="deng-b">
<div class="deng-t">新春</div>
</div>
</div>
<div class="shui shui-a">
<div class="shui-c"></div>
<div class="shui-b"></div>
</div>
</div>
</div>
</div>
<!-- 春节灯笼 -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// 获取当前日期
var currentDate = new Date();
// 定义开始日期和结束日期
var startDate = new Date('2025-01-22');//腊月廿三
var endDate = new Date('2025-02-12');//正月十五
// 获取 div 元素
var specialDiv = document.getElementById('lanterns');
// 检查当前日期是否在指定的日期范围内
if (currentDate >= startDate && currentDate <= endDate) {
// 如果在范围内,则显示 div
specialDiv.style.display = 'block';
} else {
// 否则,隐藏 div
specialDiv.style.display = 'none';
}
});
</script>
2024-12-21 15:32:27
飘舞的彩带,似灵动的画笔,在空中勾勒出绚丽色彩,为世界添一抹活力。
而烂漫樱花,如粉白的云霞,纷纷扬扬洒落,诉说着春日的温柔。彩带与樱花,在此交织出满是故事的奇妙天地,等你踏入探寻。
效果
方法
<!-- 樱花背景 -->
<script type="text/javascript" src="/assets/js/sakura.js"></script>
<!-- 丝带背景 -->
<script size="150" alpha="0.3" zindex="-2" src="/assets/js/ribbon.min.js"></script>
注意:将文件下载到自己博客使用
https://gorpeln.top/assets/js/sakura.js
https://gorpeln.top/assets/js/ribbon.min.js
2024-12-21 15:18:57
不仅能够精准显示距离特定节日的剩余时间,满足用户对重要节庆的期待与关注,还能直观展示今日、本周、本月乃至本年已悄然流逝的时间比例,帮助用户实时洞察时间的流转,让每一刻都能在时间的刻度中清晰呈现,实现对时间的高效感知与管理。
效果
方法
1、 countdown.html
<div class="countdownNav" id="countdownNav">
<div class="card-widget card-countdown">
<div class="item-headline"><i></i><span></span></div>
<div class="item-content">
<div class="cd-count-left">
<span class="cd-text">距离</span>
<span class="cd-name" id="eventName">节日</span>
<span class="cd-time" id="daysUntil">000</span>
<span class="cd-date" id="eventDate">2015-08-23</span>
</div>
<div id="countRight" class="cd-count-right">
<div id="countRight" class="cd-count-right">
<div class="cd-count-item">
<div class="cd-item-name">今日</div>
<div class="cd-item-progress"></div>
</div>
<div class="cd-count-item">
<div class="cd-item-name">本周</div>
<div class="cd-item-progress"></div>
</div>
<div class="cd-count-item">
<div class="cd-item-name">本月</div>
<div class="cd-item-progress"></div>
</div>
<div class="cd-count-item">
<div class="cd-item-name">本年</div>
<div class="cd-item-progress"></div>
</div>
</div>
</div>
</div>
</div>
</div>
2、 countdown-1.js
const CountdownTimer = (() => {
const config = {
targetDate: "2025-10-01",
targetName: "国庆",
units: {
day: { text: "今日", unit: "小时" },
week: { text: "本周", unit: "天" },
month: { text: "本月", unit: "天" },
year: { text: "本年", unit: "天" }
}
};
const calculators = {
day: () => {
const hours = new Date().getHours();
return {
remaining: 24 - hours,
percentage: (hours / 24) * 100
};
},
week: () => {
const day = new Date().getDay();
const passed = day === 0 ? 6 : day - 1;
return {
remaining: 6 - passed,
percentage: ((passed + 1) / 7) * 100
};
},
month: () => {
const now = new Date();
const total = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const passed = now.getDate() - 1;
return {
remaining: total - passed,
percentage: (passed / total) * 100
};
},
year: () => {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 1);
const total = 365 + (now.getFullYear() % 4 === 0 ? 1 : 0);
const passed = Math.floor((now - start) / 86400000);
return {
remaining: total - passed,
percentage: (passed / total) * 100
};
}
};
function updateCountdown() {
const elements = ['eventName', 'eventDate', 'daysUntil', 'countRight']
.map(id => document.getElementById(id));
if (elements.some(el => !el)) return;
const [eventName, eventDate, daysUntil, countRight] = elements;
const now = new Date();
const target = new Date(config.targetDate);
eventName.textContent = config.targetName;
eventDate.textContent = config.targetDate;
daysUntil.textContent = Math.round((target - now.setHours(0, 0, 0, 0)) / 86400000);
countRight.innerHTML = Object.entries(config.units)
.map(([key, { text, unit }]) => {
const { remaining, percentage } = calculators[key]();
return `
<div class="cd-count-item">
<div class="cd-item-name">${text}</div>
<div class="cd-item-progress">
<div class="cd-progress-bar" style="width: ${percentage}%; opacity: ${percentage}"></div>
<span class="cd-percentage ${percentage >= 46 ? 'cd-many' : ''}">${percentage.toFixed(2)}%</span>
<span class="cd-remaining ${percentage >= 60 ? 'cd-many' : ''}">
<span class="cd-tip">还剩</span>${remaining}<span class="cd-tip">${unit}</span>
</span>
</div>
</div>
`;
}).join('');
}
let timer;
const start = () => {
updateCountdown();
timer = setInterval(updateCountdown, 600000);
};
['pjax:complete', 'DOMContentLoaded'].forEach(event => document.addEventListener(event, start));
document.addEventListener('pjax:send', () => timer && clearInterval(timer));
return { start, stop: () => timer && clearInterval(timer) };
})();
优化后代码,实现倒计时包含多个时间,自动识别最近的期望时间,如果现在时间超过了所有的期望时间,就以最后一个期望时间为准。两个js二选一即可。
countdown-2.js
const CountdownTimer = (() => {
const config = {
events: [
{ targetDate: "2025-04-04", targetName: "清明节" },
{ targetDate: "2025-05-01", targetName: "劳动节" },
{ targetDate: "2025-05-31", targetName: "端午节" },
{ targetDate: "2025-10-01", targetName: "国庆节" },
{ targetDate: "2026-02-17", targetName: "春节" }
],
units: {
day: { text: "今日", unit: "小时" },
week: { text: "本周", unit: "天" },
month: { text: "本月", unit: "天" },
year: { text: "本年", unit: "天" }
}
};
const calculators = {
day: () => {
const hours = new Date().getHours();
return {
remaining: 24 - hours,
percentage: (hours / 24) * 100
};
},
week: () => {
const day = new Date().getDay();
const passed = day === 0 ? 6 : day - 1;
return {
remaining: 6 - passed,
percentage: ((passed + 1) / 7) * 100
};
},
month: () => {
const now = new Date();
const total = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const passed = now.getDate() - 1;
return {
remaining: total - passed,
percentage: (passed / total) * 100
};
},
year: () => {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 1);
const total = 365 + (now.getFullYear() % 4 === 0 ? 1 : 0);
const passed = Math.floor((now - start) / 86400000);
return {
remaining: total - passed,
percentage: (passed / total) * 100
};
}
};
function getNextEvent() {
const now = new Date();
for (let i = 0; i < config.events.length; i++) {
const eventDate = new Date(config.events[i].targetDate);
if (eventDate > now) {
return config.events[i];
}
}
return config.events[config.events.length - 1];
}
function updateCountdown() {
const elements = ['eventName', 'eventDate', 'daysUntil', 'countRight']
.map(id => document.getElementById(id));
if (elements.some(el => !el)) return;
const [eventName, eventDate, daysUntil, countRight] = elements;
const now = new Date();
const nextEvent = getNextEvent();
const target = new Date(nextEvent.targetDate);
eventName.textContent = nextEvent.targetName;
eventDate.textContent = nextEvent.targetDate;
daysUntil.textContent = Math.round((target - now.setHours(0, 0, 0, 0)) / 86400000);
countRight.innerHTML = Object.entries(config.units)
.map(([key, { text, unit }]) => {
const { remaining, percentage } = calculators[key]();
return `
<div class="cd-count-item">
<div class="cd-item-name">${text}</div>
<div class="cd-item-progress">
<div class="cd-progress-bar" style="width: ${percentage}%; opacity: ${percentage}"></div>
<span class="cd-percentage ${percentage >= 46 ? 'cd-many' : ''}">${percentage.toFixed(2)}%</span>
<span class="cd-remaining ${percentage >= 60 ? 'cd-many' : ''}">
<span class="cd-tip">还剩</span>${remaining}<span class="cd-tip">${unit}</span>
</span>
</div>
</div>
`;
}).join('');
}
let timer;
const start = () => {
updateCountdown();
timer = setInterval(updateCountdown, 600000);
};
['pjax:complete', 'DOMContentLoaded'].forEach(event => document.addEventListener(event, start));
document.addEventListener('pjax:send', () => timer && clearInterval(timer));
return { start, stop: () => timer && clearInterval(timer) };
})();
3、 style.css
/*倒计时*/
.countdownNav {
margin-top: 1em;
background-color: var(--box-color-light);
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
overflow: hidden;
width: 100%;
}
.card-countdown {
margin: 0.75em 1.5em;
}
.card-countdown .item-content {
display: flex;
}
.cd-count-left {
position: relative;
display: flex;
flex-direction: column;
margin-right: 0.8rem;
line-height: 1.5;
align-items: center;
justify-content: center;
}
.cd-count-left .cd-text {
font-size: 14px;
}
.cd-count-left .cd-name {
font-weight: bold;
font-size: 18px;
}
.cd-count-left .cd-time {
font-size: 30px;
font-weight: bold;
color: #dad9e6;
}
.cd-count-left .cd-date {
font-size: 12px;
opacity: 0.6;
}
.cd-count-left::after {
content: "";
position: absolute;
right: -0.8rem;
width: 2px;
height: 80%;
background-color: #dad9e6;
opacity: 0.5;
}
.cd-count-right {
flex: 1;
margin-left: .8rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cd-count-item {
display: flex;
flex-direction: row;
align-items: center;
height: 24px;
}
.cd-item-name {
font-size: 14px;
margin-right: 0.8rem;
white-space: nowrap;
}
.cd-item-progress {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 100%;
width: 100%;
border-radius: 8px;
background-color: var(--background-color-light);
overflow: hidden;
}
.cd-progress-bar {
height: 100%;
border-radius: 8px;
background-color: #dad9e6;
}
.cd-percentage,
.cd-remaining {
position: absolute;
font-size: 12px;
margin: 0 6px;
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.cd-many {
color: #fff;
}
.cd-remaining {
opacity: 0;
transform: translateX(10px);
}
.card-countdown .item-content:hover .cd-remaining {
transform: translateX(0);
opacity: 1;
}
.card-countdown .item-content:hover .cd-percentage {
transform: translateX(-10px);
opacity: 0;
}
2024-11-08 16:03:43
管理友链时采取手动点击检验的方式,随着时间的推移,友链数量逐渐增加,这一做法显然已不再高效。于是就需要写了一项类似API的功能,输出所有友链数据的可达性。
博客中预览:https://friends.gorpeln.top/
友链相关的常见问题
2024-10-05 11:18:56
数据无价,备份无忧。
将其他平台数据备份到不同平台的好处:
又拍云目录:
github目录:
upyun_images_sync/.github/workflows/sync-images.yml
name: Sync Images from UpYun to GitHub
on:
schedule:
- cron: '0 0 * * 3' # 每周三凌晨 0 点运行
workflow_dispatch: # 允许手动触发
jobs:
sync-images:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install gitpython
- name: Run sync script
env:
UPYUN_FTP_HOST: $
UPYUN_FTP_USER: $
UPYUN_FTP_PASSWORD: $
UPYUN_FTP_PATH: $
LOCAL_DOWNLOAD_PATH: ./tmp
REPO_PATH: ./repo
GH_TOKEN: $
REPO_OWNER: $
REPO_NAME: $
BRANCH_NAME: $
run: |
python3 sync_images.py
upyun_images_sync/sync_images.py
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
from ftplib import FTP
import git
import shutil
class MyFTP:
def __init__(self, host, port=21):
self.host = host
self.port = port
self.ftp = FTP()
self.ftp.encoding = 'utf8'
self.file_list = []
def login(self, username, password):
try:
self.ftp.connect(self.host, self.port)
self.ftp.login(username, password)
print(f"成功登录到 {self.host}")
print(f"当前工作目录: {self.ftp.pwd()}")
except Exception as err:
print(f"FTP 连接或登录失败,错误描述为:{err}")
sys.exit(1)
def download_file(self, local_file, remote_file):
if os.path.exists(local_file):
print(f"{local_file} 已存在,跳过下载")
return
try:
print(f"下载文件 {remote_file} 到 {local_file}")
buf_size = 1024
with open(local_file, 'wb') as file_handler:
self.ftp.retrbinary(f'RETR {remote_file}', file_handler.write, buf_size)
except Exception as err:
print(f"下载文件出错,出现异常:{err}")
def download_file_tree(self, local_path, remote_path):
try:
print(f"尝试切换到远程目录: {remote_path}")
self.ftp.cwd(remote_path)
print(f"成功切换到远程目录: {remote_path}")
except Exception as err:
print(f"远程目录 {remote_path} 不存在,继续... 具体错误描述为:{err}")
return
if not os.path.isdir(local_path):
os.makedirs(local_path)
print(f"创建本地目录 {local_path}")
print(f"当前工作目录: {self.ftp.pwd()}")
# 列出目录内容
remote_files = self.ftp.nlst()
print(f"远程目录内容: {remote_files}")
for remote_file in remote_files:
local = os.path.join(local_path, remote_file)
if remote_file in ['.', '..']:
continue
try:
self.ftp.cwd(remote_file)
print(f"下载目录:{remote_file}")
self.download_file_tree(local, os.path.join(remote_path, remote_file))
self.ftp.cwd("..")
except Exception:
print(f"下载文件:{remote_file}")
self.download_file(local, os.path.join(remote_path, remote_file))
def close(self):
print("FTP退出")
self.ftp.quit()
def sync_to_github(repo_path, branch_name):
if os.path.exists(repo_path):
shutil.rmtree(repo_path)
os.makedirs(repo_path)
repo_url = f"https://{GH_TOKEN}@github.com/{REPO_OWNER}/{REPO_NAME}.git"
print(f"克隆仓库: {repo_url}")
try:
repo = git.Repo.clone_from(repo_url, repo_path, branch=branch_name)
print(f"克隆仓库到 {repo_path}")
except git.exc.GitCommandError as e:
print(f"克隆仓库失败: {e}")
return
images_dir = os.path.join(repo_path, 'images')
if not os.path.exists(images_dir):
os.makedirs(images_dir)
shutil.copytree(LOCAL_DOWNLOAD_PATH, images_dir, dirs_exist_ok=True)
print(f"复制文件到 {images_dir}")
repo.git.add(all=True)
if repo.is_dirty():
repo.index.commit("Sync images from UpYun")
origin = repo.remote(name='origin')
origin.push()
print("推送更改到 GitHub 仓库")
else:
print("没有更改需要提交")
if __name__ == "__main__":
# 从环境变量中读取配置
UPYUN_FTP_HOST = os.getenv('UPYUN_FTP_HOST')
UPYUN_FTP_USER = os.getenv('UPYUN_FTP_USER')
UPYUN_FTP_PASSWORD = os.getenv('UPYUN_FTP_PASSWORD')
UPYUN_FTP_PATH = os.getenv('UPYUN_FTP_PATH', '/')
LOCAL_DOWNLOAD_PATH = os.getenv('LOCAL_DOWNLOAD_PATH', './tmp')
REPO_PATH = os.getenv('REPO_PATH', './repo')
GH_TOKEN = os.getenv('GH_TOKEN')
REPO_OWNER = os.getenv('REPO_OWNER')
REPO_NAME = os.getenv('REPO_NAME')
BRANCH_NAME = os.getenv('BRANCH_NAME', 'master')
# 创建 FTP 对象
my_ftp = MyFTP(UPYUN_FTP_HOST)
# 登录 FTP 服务器
my_ftp.login(UPYUN_FTP_USER, UPYUN_FTP_PASSWORD)
# 下载目录
my_ftp.download_file_tree(LOCAL_DOWNLOAD_PATH, UPYUN_FTP_PATH)
# 关闭 FTP 连接
my_ftp.close()
# 同步到 GitHub
sync_to_github(REPO_PATH, BRANCH_NAME)
将本地的项目提交到github,然后设置Workflow 读写权限
在github上设置Secrets 变量
序号 | 变量名 | 释义 |
---|---|---|
1 | UPYUN_FTP_HOST | 又拍云 FTP 主机地址 |
2 | UPYUN_FTP_USER | 又拍云 FTP 用户名 |
3 | UPYUN_FTP_PASSWORD | 又拍云 FTP 密码 |
4 | UPYUN_FTP_PATH | 又拍云 FTP 路径 |
5 | GH_TOKEN | GitHub 个人访问令牌 |
6 | REPO_OWNER | 仓库所有者 |
7 | REPO_NAME | 仓库名称 |
8 | BRANCH_NAME | 分支名称(默认为 master) |
变量详解:
UPYUN_FTP_HOST
:参考又拍云文档,直接使用v0.ftp.upyun.com
就行了UPYUN_FTP_USER
:格式为 operator/bucket,在又拍云 - 云存储 - 选择对应的bucket点击配置 - 存储管理 - 操作员授权 - 自己添加操作用户名和密码,假设你的又拍云云储存bucket的名称为upai-img,自定义的用户名为user,密码为123456,则UPYUN_FTP_USER就是 user/upai-img
,如果你看不懂我说的,参考 又拍云视频教程
UPYUN_FTP_PASSWORD
:就是上面你自己设置的 123456
,图片示例
UPYUN_FTP_PATH
:直接使用 /
(根目录)就行了GH_TOKEN
:选择github的 Settings - 点击右侧列表最下面的 Developer Settings - Personal access tokens - tokens (classic) - Generated new token (classic),Note名字可以随意,Expiration时间选择 no Expiration,下面权限全选了,点击Generated token 就生成了想要的token,记得保存,他只显示一次,示例:abc_123456789pUS123454321WmJkE987654321
,图片示例
REPO_OWNER
:就是你的github的用户名,我在github给这个项目创建仓库为https://github.com/gorpeln/upyun_images_sync,示例:gorpeln
REPO_NAME
:仓库链接后面的就是仓库名,示例:upyun_images_sync
,图片示例
BRANCH_NAME
:项目分支名,一般为master/main,示例:master