优化离线包体积
This commit is contained in:
@@ -171,8 +171,9 @@ func NewImageStreamer(config *ImageStreamerConfig) *ImageStreamer {
|
|||||||
|
|
||||||
// StreamOptions 下载选项
|
// StreamOptions 下载选项
|
||||||
type StreamOptions struct {
|
type StreamOptions struct {
|
||||||
Platform string
|
Platform string
|
||||||
Compression bool
|
Compression bool
|
||||||
|
UseCompressedLayers bool // 是否保存原始压缩层,默认true
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamImageToWriter 流式下载镜像到Writer
|
// StreamImageToWriter 流式下载镜像到Writer
|
||||||
@@ -336,12 +337,24 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
uncompressedSize, err := partial.UncompressedSize(layer)
|
var layerSize int64
|
||||||
if err != nil {
|
var layerReader io.ReadCloser
|
||||||
return err
|
|
||||||
|
// 根据配置选择使用压缩层或未压缩层
|
||||||
|
if options != nil && options.UseCompressedLayers {
|
||||||
|
layerSize, err = layer.Size()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layerReader, err = layer.Compressed()
|
||||||
|
} else {
|
||||||
|
layerSize, err = partial.UncompressedSize(layer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layerReader, err = layer.Uncompressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
layerReader, err := layer.Uncompressed()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -349,7 +362,7 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr
|
|||||||
|
|
||||||
layerTarHeader := &tar.Header{
|
layerTarHeader := &tar.Header{
|
||||||
Name: layerDir + "/layer.tar",
|
Name: layerDir + "/layer.tar",
|
||||||
Size: uncompressedSize,
|
Size: layerSize,
|
||||||
Mode: 0644,
|
Mode: 0644,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,6 +641,7 @@ func handleDirectImageDownload(c *gin.Context) {
|
|||||||
imageRef := strings.ReplaceAll(imageParam, "_", "/")
|
imageRef := strings.ReplaceAll(imageParam, "_", "/")
|
||||||
platform := c.Query("platform")
|
platform := c.Query("platform")
|
||||||
tag := c.DefaultQuery("tag", "")
|
tag := c.DefaultQuery("tag", "")
|
||||||
|
useCompressed := c.DefaultQuery("compressed", "true") == "true"
|
||||||
|
|
||||||
if tag != "" && !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "@") {
|
if tag != "" && !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "@") {
|
||||||
imageRef = imageRef + ":" + tag
|
imageRef = imageRef + ":" + tag
|
||||||
@@ -653,8 +667,9 @@ func handleDirectImageDownload(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options := &StreamOptions{
|
options := &StreamOptions{
|
||||||
Platform: platform,
|
Platform: platform,
|
||||||
Compression: false,
|
Compression: false,
|
||||||
|
UseCompressedLayers: useCompressed,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
@@ -670,8 +685,9 @@ func handleDirectImageDownload(c *gin.Context) {
|
|||||||
// handleSimpleBatchDownload 处理批量下载
|
// handleSimpleBatchDownload 处理批量下载
|
||||||
func handleSimpleBatchDownload(c *gin.Context) {
|
func handleSimpleBatchDownload(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Images []string `json:"images" binding:"required"`
|
Images []string `json:"images" binding:"required"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
|
UseCompressedLayers *bool `json:"useCompressedLayers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -710,9 +726,15 @@ func handleSimpleBatchDownload(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useCompressed := true // 默认启用原始压缩层
|
||||||
|
if req.UseCompressedLayers != nil {
|
||||||
|
useCompressed = *req.UseCompressedLayers
|
||||||
|
}
|
||||||
|
|
||||||
options := &StreamOptions{
|
options := &StreamOptions{
|
||||||
Platform: req.Platform,
|
Platform: req.Platform,
|
||||||
Compression: false,
|
Compression: false,
|
||||||
|
UseCompressedLayers: useCompressed,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|||||||
@@ -399,6 +399,67 @@
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 切换开关样式 */
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--muted);
|
||||||
|
transition: 0.2s;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.2s;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -559,6 +620,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="switch-container">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="compressedToggle" checked>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<label for="compressedToggle" class="switch-label">使用压缩层(减小包体积)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="downloadBtn">
|
<button type="submit" class="btn btn-primary btn-full" id="downloadBtn">
|
||||||
<span id="downloadText">立即下载</span>
|
<span id="downloadText">立即下载</span>
|
||||||
<span id="downloadLoading" class="loading hidden"></span>
|
<span id="downloadLoading" class="loading hidden"></span>
|
||||||
@@ -595,6 +664,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="switch-container">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="batchCompressedToggle" checked>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<label for="batchCompressedToggle" class="switch-label">使用压缩层(减小包体积)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="batchDownloadBtn">
|
<button type="submit" class="btn btn-primary btn-full" id="batchDownloadBtn">
|
||||||
<span id="batchDownloadText">开始下载</span>
|
<span id="batchDownloadText">开始下载</span>
|
||||||
<span id="batchDownloadLoading" class="loading hidden"></span>
|
<span id="batchDownloadLoading" class="loading hidden"></span>
|
||||||
@@ -651,12 +728,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDownloadUrl(imageName, platform = '') {
|
function buildDownloadUrl(imageName, platform = '', useCompressed = true) {
|
||||||
const encodedImage = imageName.replace(/\//g, '_');
|
const encodedImage = imageName.replace(/\//g, '_');
|
||||||
let url = `/api/image/download/${encodedImage}`;
|
let url = `/api/image/download/${encodedImage}`;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
if (platform && platform.trim()) {
|
if (platform && platform.trim()) {
|
||||||
url += `?platform=${encodeURIComponent(platform.trim())}`;
|
params.append('platform', platform.trim());
|
||||||
|
}
|
||||||
|
params.append('compressed', useCompressed.toString());
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
url += '?' + params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
@@ -672,11 +755,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const platform = document.getElementById('platformInput').value.trim();
|
const platform = document.getElementById('platformInput').value.trim();
|
||||||
|
const useCompressed = document.getElementById('compressedToggle').checked;
|
||||||
|
|
||||||
hideStatus('singleStatus');
|
hideStatus('singleStatus');
|
||||||
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', true);
|
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', true);
|
||||||
|
|
||||||
const downloadUrl = buildDownloadUrl(imageName, platform);
|
const downloadUrl = buildDownloadUrl(imageName, platform, useCompressed);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
@@ -711,9 +795,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const platform = document.getElementById('batchPlatformInput').value.trim();
|
const platform = document.getElementById('batchPlatformInput').value.trim();
|
||||||
|
const useCompressed = document.getElementById('batchCompressedToggle').checked;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
images: images
|
images: images,
|
||||||
|
useCompressedLayers: useCompressed
|
||||||
};
|
};
|
||||||
|
|
||||||
if (platform) {
|
if (platform) {
|
||||||
|
|||||||
Reference in New Issue
Block a user