全部產品
Search
文件中心

Object Storage Service:斷點續傳上傳

更新時間:Jun 19, 2024

在上傳大檔案(超過5 GB)到OSS的過程中,如果出現網路中斷、程式異常退出等問題導致檔案上傳失敗,甚至重試多次仍無法完成上傳,您需要使用斷點續傳上傳的方式。斷點續傳上傳將需要上傳的大檔案分成多個較小的分區並發上傳,加速上傳完成時間。如果上傳過程中,某一分區上傳失敗,再次上傳時會從Checkpoint檔案記錄的斷點繼續上傳,無需重新上傳所有分區。上傳完成後,所有分區將合并成完整的檔案。

前提條件

已建立儲存空間(Bucket)。詳情請參見控制台建立儲存空間

注意事項

  • 本文以華東1(杭州)外網Endpoint為例。如果您希望通過與OSS同地區的其他阿里雲產品訪問OSS,請使用內網Endpoint。關於OSS支援的Region與Endpoint的對應關係,請參見訪問網域名稱和資料中心

  • 要斷點續傳上傳,您必須有oss:PutObject許可權。具體操作,請參見為RAM使用者授權自訂的權限原則

  • SDK會將上傳的狀態資訊記錄在Checkpoint檔案中,所以要確保程式對Checkpoint檔案有寫入權限。

  • 請勿修改Checkpoint檔案中攜帶的校正資訊。如果Checkpoint檔案損壞,則會重新上傳所有分區。

  • 如果上傳過程中本地檔案發生了改變,則會重新上傳所有分區。

使用阿里雲SDK

以下僅列舉常見SDK的斷點續傳上傳的程式碼範例。關於其他SDK的斷點續傳上傳的程式碼範例,請參見SDK簡介

import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.*;

public class Demo {
    public static void main(String[] args) {
        // Endpoint以華東1(杭州)為例,其它Region請按實際情況填寫。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        // 建立OSSClient執行個體。
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
        try {

            ObjectMetadata meta = new ObjectMetadata();
            // 指定上傳的內容類型。
            meta.setContentType("text/plain");

            // 檔案上傳時設定存取權限ACL。
            // meta.setObjectAcl(CannedAccessControlList.Private);

            // 通過UploadFileRequest設定多個參數。
            // 依次填寫Bucket名稱(例如examplebucket)以及Object完整路徑(例如exampledir/exampleobject.txt),Object完整路徑中不能包含Bucket名稱。
            UploadFileRequest uploadFileRequest = new UploadFileRequest("examplebucket","exampledir/exampleobject.txt");

            // 通過UploadFileRequest設定單個參數。           
            // 填寫本地檔案的完整路徑,例如D:\\localpath\\examplefile.txt。如果未指定本地路徑,則預設從樣本程式所屬專案對應本地路徑中上傳檔案。
            uploadFileRequest.setUploadFile("D:\\localpath\\examplefile.txt");
            // 指定上傳並發線程數,預設值為1。
            uploadFileRequest.setTaskNum(5);
            // 指定上傳的分區大小,單位為位元組,取值範圍為100 KB~5 GB。預設值為100 KB。
            uploadFileRequest.setPartSize(1 * 1024 * 1024);
            // 開啟斷點續傳,預設關閉。
            uploadFileRequest.setEnableCheckpoint(true);
            // 記錄本地分區上傳結果的檔案。上傳過程中的進度資訊會儲存在該檔案中,如果某一分區上傳失敗,再次上傳時會根據檔案中記錄的點繼續上傳。上傳完成後,該檔案會被刪除。
            // 如果未設定該值,預設與待上傳的本地檔案同路徑,名稱為${uploadFile}.ucp。
            uploadFileRequest.setCheckpointFile("yourCheckpointFile");
            // 檔案的中繼資料。
            uploadFileRequest.setObjectMetadata(meta);
            // 設定上傳回調,參數為Callback類型。
            //uploadFileRequest.setCallback("yourCallbackEvent");

            // 斷點續傳上傳。
            ossClient.uploadFile(uploadFileRequest);

        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (Throwable ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
                // 關閉OSSClient。
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}           
const OSS = require('ali-oss')

const client = new OSS({
  // yourregion填寫Bucket所在地區。以華東1(杭州)為例,Region填寫為oss-cn-hangzhou。
  region: 'yourregion',
  // 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  // 填寫Bucket名稱。
  bucket: 'examplebucket'
});

// yourfilepath填寫已上傳檔案所在的本地路徑。
const filePath = "yourfilepath";

let checkpoint;
async function resumeUpload() {
  // 重試五次。
  for (let i = 0; i < 5; i++) {
    try {
      const result = await client.multipartUpload('object-name', filePath, {
        checkpoint,
        async progress(percentage, cpt) {
          checkpoint = cpt;
        },
      });
      console.log(result);
      break; // 跳出當前迴圈。
    } catch (e) {
      console.log(e);
    }
  }
}

resumeUpload();
# -*- coding: utf-8 -*-
import oss2
from oss2.credentials import EnvironmentVariableCredentialsProvider
# 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())
# yourEndpoint填寫Bucket所在地區對應的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
# 填寫Bucket名稱,例如examplebucket。
bucket = oss2.Bucket(auth, 'https://oss-cn-hangzhou.aliyuncs.com', 'examplebucket')

# yourObjectName填寫Object完整路徑,完整路徑中不能包含Bucket名稱,例如exampledir/exampleobject.txt。
# yourLocalFile填寫本地檔案的完整路徑,例如D:\\localpath\\examplefile.txt。如果未指定本地路徑,則預設從樣本程式所屬專案對應本地路徑中上傳檔案。
oss2.resumable_upload(bucket, 'exampledir/exampleobject.txt', 'D:\\localpath\\examplefile.txt')
# 如未使用參數store指定目錄,則會在HOME目錄下建立.py-oss-upload目錄來儲存斷點資訊。

# Python SDK 2.1.0以上版本支援斷點續傳上傳時設定以下選擇性參數。
# import sys
# # 當無法確定待上傳的資料長度時,total_bytes的值為None。
# def percentage(consumed_bytes, total_bytes):
#     if total_bytes:
#         rate = int(100 * (float(consumed_bytes) / float(total_bytes)))
#         print('\r{0}% '.format(rate), end='')
#         sys.stdout.flush()
# # 如果使用store指定了目錄,則斷點資訊將儲存在指定目錄中。如果使用num_threads設定並發上傳線程數,請將oss2.defaults.connection_pool_size設定為大於或等於並發上傳線程數。預設並發上傳線程數為1。
# oss2.resumable_upload(bucket, '<yourObjectName>', '<yourLocalFile>',
#                       store=oss2.ResumableStore(root='/tmp'),
#                       # 指定當檔案長度大於或等於選擇性參數multipart_threshold(預設值為10 MB)時,則使用分區上傳。
#                       multipart_threshold=100*1024,
#                       # 設定分區大小,單位為位元組,取值範圍為100 KB~5 GB。預設值為100 KB。
#                       part_size=100*1024,
#                       # 設定上傳回調進度函數。
#                       progress_callback=percentage,
#                       # 如果使用num_threads設定並發上傳線程數,請將oss2.defaults.connection_pool_size設定為大於或等於並發上傳線程數。預設並發上傳線程數為1。
#                       num_threads=4)
using Aliyun.OSS;
using Aliyun.OSS.Common;

// yourEndpoint填寫Bucket所在地區對應的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
var endpoint = "yourEndpoint";
// 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
var accessKeyId = Environment.GetEnvironmentVariable("OSS_ACCESS_KEY_ID");
var accessKeySecret = Environment.GetEnvironmentVariable("OSS_ACCESS_KEY_SECRET");
// 填寫Bucket名稱,例如examplebucket。
var bucketName = "examplebucket";
// 填寫Object完整路徑,Object完整路徑中不能包含Bucket名稱,例如exampledir/exampleobject.txt。
var objectName = "exampledir/exampleobject.txt";
// 填寫本地檔案的完整路徑,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路徑只填寫了檔案名稱(例如examplefile.txt),則預設從樣本程式所屬專案對應本地路徑中上傳檔案。
var localFilename = "D:\\localpath\\examplefile.txt";
// 記錄本地分區上傳結果的檔案。上傳過程中的進度資訊會儲存在該檔案中。
string checkpointDir = "yourCheckpointDir";
// 建立OssClient執行個體。
var client = new OssClient(endpoint, accessKeyId, accessKeySecret);
try
{
    // 通過UploadFileRequest設定多個參數。
    UploadObjectRequest request = new UploadObjectRequest(bucketName, objectName, localFilename)
    {
        // 指定上傳的分區大小。
        PartSize = 8 * 1024 * 1024,
        // 指定並發線程數。
        ParallelThreadCount = 3,
        // checkpointDir儲存斷點續傳的中間狀態,用於失敗後繼續上傳。
        // 如果checkpointDir為null,斷點續傳功能不會生效,每次失敗後都會重新上傳。
        CheckpointDir = checkpointDir,
    };
    // 斷點續傳上傳。
    client.ResumableUploadObject(request);
    Console.WriteLine("Resumable upload object:{0} succeeded", objectName);
}
catch (OssException ex)
{
    Console.WriteLine("Failed with error code: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}",
        ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId);
}
catch (Exception ex)
{
    Console.WriteLine("Failed with error info: {0}", ex.Message);
}
// 填寫Bucket名稱,例如examplebucket。
String bucketName = "examplebucket";
// 填寫Object完整路徑,例如exampledir/exampleobject.txt。Object完整路徑中不能包含Bucket名稱。
String objectName = "exampledir/exampleobject.txt";
// 填寫檔案完整路徑,例如/storage/emulated/0/oss/examplefile.txt。
String localFilepath = "/storage/emulated/0/oss/examplefile.txt";

String recordDirectory = Environment.getExternalStorageDirectory().getAbsolutePath() + "/oss_record/";

File recordDir = new File(recordDirectory);

// 確保斷點記錄的儲存檔案夾已存在,如果不存在則建立斷點記錄的儲存檔案夾。
if (!recordDir.exists()) {
    recordDir.mkdirs();
}

// 建立斷點續傳上傳請求,並指定斷點記錄檔案的儲存路徑,儲存路徑為斷點記錄檔案的絕對路徑。
ResumableUploadRequest request = new ResumableUploadRequest(bucketName, objectName, localFilepath, recordDirectory);
// 調用OSSAsyncTask cancel()方法時,設定DeleteUploadOnCancelling為false,表示不刪除斷點記錄檔案,下次再上傳同一個檔案時將從斷點記錄處繼續上傳。如果不設定此參數,則預設值為true,表示刪除斷點記錄檔案,下次再上傳同一個檔案時則重新上傳。
request.setDeleteUploadOnCancelling(false);
// 設定上傳回調。
request.setProgressCallback(new OSSProgressCallback<ResumableUploadRequest>() {
    @Override
    public void onProgress(ResumableUploadRequest request, long currentSize, long totalSize) {
        Log.d("resumableUpload", "currentSize: " + currentSize + " totalSize: " + totalSize);
    }
});


ResumableUploadResult uploadResult = oss.resumableUpload(request);
package main

import (
    "fmt"
    "os"
    "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func main() {
    // 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
    provider, err := oss.NewEnvironmentVariableCredentialsProvider()
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }

    // 建立OSSClient執行個體。
    // yourEndpoint填寫Bucket對應的Endpoint,以華東1(杭州)為例,填寫為https://oss-cn-hangzhou.aliyuncs.com。其它Region請按實際情況填寫。
    client, err := oss.New("yourEndpoint", "", "", oss.SetCredentialsProvider(&provider))
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }

    // 填寫Bucket名稱,例如examplebucket。
    bucket, err := client.Bucket("examplebucket")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }

    // 通過UploadFile實現斷點續傳上傳時,限制分區數量不能超過10000。
    // 您需要結合上傳檔案的大小,合理設定每個分區的大小。每個分區大小的取值範圍為100 KB~5 GB。預設值為100 KB(即100*1024)。
    // 通過oss.Routines指定分區上傳並發數為3。
    // yourObjectName填寫Object完整路徑,完整路徑中不能包含Bucket名稱,例如exampledir/exampleobject.txt。
    // yourLocalFile填寫本地檔案的完整路徑,例如D:\\localpath\\examplefile.txt。如果未指定本地路徑,則預設從樣本程式所屬專案對應本地路徑中上傳檔案。
    err = bucket.UploadFile("exampledir/exampleobject.txt", "D:\\localpath\\examplefile.txt", 100*1024, oss.Routines(3), oss.Checkpoint(true, ""))
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(-1)
    }
}            
// 擷取UploadId上傳檔案。
OSSResumableUploadRequest * resumableUpload = [OSSResumableUploadRequest new];
resumableUpload.bucketName = <bucketName>;
// objectKey等同於objectName,表示斷點上傳檔案到OSS時需要指定包含檔案尾碼在內的完整路徑,例如abc/efg/123.jpg
resumableUpload.objectKey = <objectKey>;
resumableUpload.partSize = 1024 * 1024;
resumableUpload.uploadProgress = ^(int64_t bytesSent, int64_t totalByteSent, int64_t totalBytesExpectedToSend) {
    NSLog(@"%lld, %lld, %lld", bytesSent, totalByteSent, totalBytesExpectedToSend);
};
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 設定斷點記錄儲存路徑。
resumableUpload.recordDirectoryPath = cachesDir;
// 將參數deleteUploadIdOnCancelling設定為NO,表示不刪除斷點記錄檔案,上傳失敗後將從斷點記錄處繼續上傳直到檔案上傳完成。如果不設定此參數,即保留預設值YES,表示刪除斷點記錄檔案,下次再上傳同一檔案時則重新上傳。
resumableUpload.deleteUploadIdOnCancelling = NO;

resumableUpload.uploadingFileURL = [NSURL fileURLWithPath:<your file path>];
OSSTask * resumeTask = [client resumableUpload:resumableUpload];
[resumeTask continueWithBlock:^id(OSSTask *task) {
    if (task.error) {
        NSLog(@"error: %@", task.error);
        if ([task.error.domain isEqualToString:OSSClientErrorDomain] && task.error.code == OSSClientErrorCodeCannotResumeUpload) {
            // 此任務無法續傳,需擷取新的uploadId重新上傳。
        }
    } else {
        NSLog(@"Upload file success");
    }
    return nil;
}];

// [resumeTask waitUntilFinished];

// [resumableUpload cancel];
                    
#include <alibabacloud/oss/OssClient.h>
using namespace AlibabaCloud::OSS;

int main(void)
{
    /* 初始化OSS帳號資訊。*/
            
    /* yourEndpoint填寫Bucket所在地區對應的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。*/
    std::string Endpoint = "yourEndpoint";
    /* 填寫Bucket名稱,例如examplebucket。*/
    std::string BucketName = "examplebucket";
    /* 填寫Object的完整路徑,完整路徑中不能包含Bucket名稱,例如exampledir/exampleobject.txt。*/
    std::string ObjectName = "exampledir/exampleobject.txt";
    /* 填寫本地檔案的完整路徑,例如D:\\localpath\\examplefile.txt。如果未指定本地路徑,則預設從樣本程式所屬專案對應本地路徑中上傳檔案。*/
    std::string UploadFilePath = "D:\\localpath\\examplefile.txt";
    /* 記錄本地分區上傳結果的檔案。上傳過程中的進度資訊會儲存在該檔案中,如果某一分區上傳失敗,再次上傳時會根據檔案中記錄的斷點繼續上傳。上傳完成後,該檔案會被刪除。*/
    /* 設定斷點記錄檔案所在的目錄,並確保指定的目錄已存在,例如D:\\local。如果未設定該值,預設與待上傳的本地檔案同路徑。*/
    std::string CheckpointFilePath = "D:\\local";

    /* 初始化網路等資源。*/
    InitializeSdk();

    ClientConfiguration conf;
    /* 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。*/
    auto credentialsProvider = std::make_shared<EnvironmentVariableCredentialsProvider>();
    OssClient client(Endpoint, credentialsProvider, conf);

    /* 斷點續傳上傳。*/
    UploadObjectRequest request(BucketName, ObjectName, UploadFilePath, CheckpointFilePath);
    auto outcome = client.ResumableUploadObject(request);

    if (!outcome.isSuccess()) {
        /* 異常處理。*/
        std::cout << "ResumableUploadObject fail" <<
        ",code:" << outcome.error().Code() <<
        ",message:" << outcome.error().Message() <<
        ",requestId:" << outcome.error().RequestId() << std::endl;
        return -1;
    }

    /* 釋放網路等資源。*/
    ShutdownSdk();
    return 0;
}
#include "oss_api.h"
#include "aos_http_io.h"
/* yourEndpoint填寫Bucket所在地區對應的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。*/
const char *endpoint = "yourEndpoint";

/* 填寫Bucket名稱,例如examplebucket。*/
const char *bucket_name = "examplebucket";
/* 填寫Object完整路徑,完整路徑中不能包含Bucket名稱,例如exampledir/exampleobject.txt。*/
const char *object_name = "exampledir/exampleobject.txt";
/* 填寫本地檔案的完整路徑。*/
const char *local_filename = "yourLocalFilename";
void init_options(oss_request_options_t *options)
{
    options->config = oss_config_create(options->pool);
    /* 用char*類型的字串初始化aos_string_t類型。*/
    aos_str_set(&options->config->endpoint, endpoint);
    /* 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。*/    
    aos_str_set(&options->config->access_key_id, getenv("OSS_ACCESS_KEY_ID"));
    aos_str_set(&options->config->access_key_secret, getenv("OSS_ACCESS_KEY_SECRET"));
    /* 是否使用了CNAME。0表示不使用。*/
    options->config->is_cname = 0;
    /* 設定網路相關參數,比如逾時時間等。*/
    options->ctl = aos_http_controller_create(options->pool, 0);
}
int main(int argc, char *argv[])
{
    /* 在程式入口調用aos_http_io_initialize方法來初始化網路、記憶體等全域資源。*/
    if (aos_http_io_initialize(NULL, 0) != AOSE_OK) {
        exit(1);
    }
    /* 用於記憶體管理的記憶體池(pool),等價於apr_pool_t。其實現代碼在apr庫中。*/
    aos_pool_t *pool;
    /* 重新建立一個新的記憶體池,第二個參數是NULL,表示沒有繼承其它記憶體池。*/
    aos_pool_create(&pool, NULL);
    /* 建立並初始化options,該參數包括endpoint、access_key_id、acces_key_secret、is_cname、curl等全域配置資訊。*/
    oss_request_options_t *oss_client_options;
    /* 在記憶體池中分配記憶體給options。*/
    oss_client_options = oss_request_options_create(pool);
    /* 初始化Client的選項oss_client_options。*/
    init_options(oss_client_options);
    /* 初始化參數。*/
    aos_string_t bucket;
    aos_string_t object;
    aos_string_t file;
    aos_list_t resp_body;
    aos_table_t *headers = NULL;
    aos_table_t *resp_headers = NULL; 
    aos_status_t *resp_status = NULL; 
    oss_resumable_clt_params_t *clt_params;
    aos_str_set(&bucket, bucket_name);
    aos_str_set(&object, object_name);
    aos_str_set(&file, local_filename);
    aos_list_init(&resp_body);
    /* 斷點續傳。*/
    clt_params = oss_create_resumable_clt_params_content(pool, 1024 * 100, 3, AOS_TRUE, NULL);
    resp_status = oss_resumable_upload_file(oss_client_options, &bucket, &object, &file, headers, NULL, clt_params, NULL, &resp_headers, &resp_body);
    if (aos_status_is_ok(resp_status)) {
        printf("resumable upload succeeded\n");
    } else {
        printf("resumable upload failed\n");
    }
    /* 釋放記憶體池,相當於釋放了請求過程中各資源分派的記憶體。*/
    aos_pool_destroy(pool);
    /* 釋放之前分配的全域資源。*/
    aos_http_io_deinitialize();
    return 0;
}
require 'aliyun/oss'

client = Aliyun::OSS::Client.new(
  # Endpoint以華東1(杭州)為例,其它Region請按實際情況填寫。
  endpoint: 'https://oss-cn-hangzhou.aliyuncs.com',
  # 從環境變數中擷取訪問憑證。運行本程式碼範例之前,請確保已設定環境變數OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
  access_key_id: ENV['OSS_ACCESS_KEY_ID'],
  access_key_secret: ENV['OSS_ACCESS_KEY_SECRET']
)
# 填寫Bucket名稱,例如examplebucket。
bucket = client.get_bucket('examplebucket')
# key填寫Object完整路徑,Object完整路徑中不能包含Bucket名稱,例如exampledir/example.zip。
# file填寫本地檔案的完整路徑,例如/tmp/example.zip。
bucket.resumable_upload('exampledir/example.zip', '/tmp/example.zip') do |p|
  puts "Progress: #{p}"
end

bucket.resumable_upload(
  'exampledir/example.zip', '/tmp/example.zip',
  :part_size => 100 * 1024, :cpt_file => '/tmp/example.zip.cpt') { |p|
  puts "Progress: #{p}"
}