使用Cloudflare Worker的免费账户限制R2的支出(新版本更新使用D1数据库限制次数)

众所周知CF R2提供了10GB了免费空间和每月一千万次的免费Class B操作。
也就是说如果开一个存储桶,并且设置为公开的话,每个月被刷一千万次之后就开始收费,而且上不封顶,没有限制的方法。
为了这个存储桶能够被外界访问到但是同时不会一夜破产,可以创建一个CF Worker把私有桶映射为公开桶+D1数据库记录使用次数的组合作为限制,让一个账单周期月的请求次数限制在选定的范围内

感谢佬友提醒Worker每天100000次之后不一定会停,现在的版本使用D1数据库记录使用次数。
所以每个请求会消耗1个Worker请求次数+一个D1读次数+一个D1写次数
Worker免费套餐的瓶颈依旧在每天100000次,超过了能不能用看Cloudflare心情。

export default {
    async fetch(request, env) {
        const path = decodeURIComponent(new URL(request.url).pathname.slice(1));
        if (!path) {
            return new Response("Not Found", { status: 404 });
        }

        try {
            const limitCheckResult = await checkUsageLimit(env);
            if (limitCheckResult.limited) {
                return new Response("Monthly usage limit exceeded", { status: 429 });
            }

            const file = await env.OPCT.get(path);
            if (!file) {
                return new Response("Not Found", { status: 404 });
            }

            const headers = new Headers();
            headers.set('Content-Type', file.httpMetadata?.contentType || 'application/octet-stream');

            return new Response(file.body, { headers });
        } catch (error) {
            return new Response("Server Error", { status: 500 });
        }
    }
}

async function checkUsageLimit(env) {
    const billingResetDay = parseInt(env.BILLING_RESET_DAY);
    const monthlyLimit = parseInt(env.MONTHLY_LIMIT);

    const billingStartDay = calculateBillingPeriod(billingResetDay);

    const usageRecord = await env.DB.prepare(
        "SELECT * FROM usage_records LIMIT 1"
    ).first();

    let currentUsage = 1;

    if (!usageRecord) {
        await env.DB.prepare(
            "INSERT INTO usage_records (usage_count, billing_period) VALUES (?, ?)"
        ).bind(currentUsage, billingStartDay).run();
    } else if (usageRecord.billing_period !== billingStartDay) {
        await env.DB.prepare(
            "UPDATE usage_records SET usage_count = ?, billing_period = ?"
        ).bind(currentUsage, billingStartDay).run();
    } else {
        currentUsage = usageRecord.usage_count + 1;

        if (currentUsage > monthlyLimit) {
            return { limited: true };
        }

        await env.DB.prepare(
            "UPDATE usage_records SET usage_count = ?"
        ).bind(currentUsage).run();
    }

    return { limited: false };
}

function calculateBillingPeriod(billingResetDay) {
    const now = new Date();
    const currentDay = now.getUTCDate();
    const currentMonth = now.getUTCMonth() + 1;
    const currentYear = now.getUTCFullYear();

    const lastDayOfMonth = new Date(now.getUTCFullYear(), now.getUTCMonth() + 1, 0).getUTCDate();

    const actualResetDay = Math.min(billingResetDay, lastDayOfMonth);

    let targetMonth = currentMonth;
    let targetYear = currentYear;

    if (currentDay < actualResetDay) {
        targetMonth = targetMonth - 1;
        if (targetMonth === 0) {
            targetMonth = 12;
            targetYear = targetYear - 1;
        }
    }

    const lastDayOfPrevMonth = new Date(targetYear, targetMonth, 0).getUTCDate();
    const actualPrevMonthResetDay = Math.min(billingResetDay, lastDayOfPrevMonth);

    return `${targetYear}-${targetMonth.toString().padStart(2, '0')}-${actualPrevMonthResetDay.toString().padStart(2, '0')}`;
}

设置绑定那里把桶绑定到OPCT名称上就行,D1数据库绑定到DB

数据库创建在console那里填入:

CREATE TABLE usage_records (
    usage_count INTEGER NOT NULL,
    billing_period TEXT NOT NULL
);

环境变量:
BILLING_RESET_DAY:R2的账单重置日,在账单那里看
MONTHLY_LIMIT:每月请求次数限制

另外Worker的放置位置保持在默认就行,选择智能的话反而Worker全都运行在桶的地理位置

另外如果不是为了白嫖,那Backblaze B2的存储价格低一些

69 Likes

感谢感谢,用起来

7 Likes

感谢分享。学习了。

5 Likes

感谢佬友的分享,用起来用起来

3 Likes

感谢大佬

3 Likes

mark一下

2 Likes

感谢分享,学到了

3 Likes

好清奇的思路,莫非佬友是天才!

3 Likes

感谢佬,刚刚去实验一下的时候看到官方有给样例,佬的这个跟那个有什么区别吗

1 Like

哪个,可以发出来看看吗

1 Like

我看它还提供了 Python 上传样例

这里和上传无关,只是一个下载的中间层,用worker免费的使用次数去限制下载次数而已

1 Like

上传直接用s3 api就行,没理由用worker

1 Like

佬,worker每天超过10万就会自动停止吗?

1 Like

对,达到10w停止

2 Likes

事实上请求来到10万并不一定会停止。

https://linux.do/t/topic/263043

1 Like

卧槽,我不知道这回事

最近一直在考虑对象存储的问题,感谢分享。

这招确实稳,我去年就被刷爆过账单。用worker限流后安心多了,不过记得开个低额告警,防万一。

感谢分享w