Skip to content

Commit 6e2358f

Browse files
authored
feat: add folder upload support with improved code structure (#4)
* feat: enhance Vercel Blob Action to support folder uploads and improve documentation - Updated action.yml to clarify input descriptions for file and folder uploads. - Modified README to reflect the ability to upload entire folders, including detailed usage examples. - Refactored index.ts to implement recursive file retrieval for folder uploads, enhancing functionality. - Improved error handling to differentiate between file and directory uploads, providing clearer error messages. * feat: add GitHub Actions workflow for testing file and folder uploads to Vercel Blob - Introduced a new workflow in test-upload.yml to automate testing of single file and folder uploads to Vercel Blob. - The workflow includes steps for creating test files and folders, uploading them, and summarizing the test results. - Implemented cleanup steps to remove test artifacts after execution.
1 parent eda48fa commit 6e2358f

File tree

6 files changed

+293
-59
lines changed

6 files changed

+293
-59
lines changed

.github/workflows/test-upload.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Test Upload Functionality
2+
3+
on: [push]
4+
5+
jobs:
6+
test-single-file:
7+
runs-on: ubuntu-latest
8+
name: Test Single File Upload
9+
10+
steps:
11+
- name: Checkout code
12+
uses: actions/checkout@v4
13+
14+
- name: Create test file
15+
run: |
16+
echo "Test file created at $(date)" > test-file.txt
17+
echo "GitHub SHA: ${{ github.sha }}" >> test-file.txt
18+
echo "Workflow run: ${{ github.run_number }}" >> test-file.txt
19+
echo "Test type: Single file upload" >> test-file.txt
20+
21+
- name: Upload single file to Vercel Blob
22+
uses: ./
23+
with:
24+
source: "test-file.txt"
25+
destination: "github-actions-test/single-file-${{ github.run_number }}.txt"
26+
read-write-token: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
27+
28+
- name: Cleanup
29+
run: rm -f test-file.txt
30+
31+
test-folder-upload:
32+
runs-on: ubuntu-latest
33+
name: Test Folder Upload
34+
35+
steps:
36+
- name: Checkout code
37+
uses: actions/checkout@v4
38+
39+
- name: Create test folder structure
40+
run: |
41+
mkdir -p test-folder/assets/css
42+
mkdir -p test-folder/assets/js
43+
mkdir -p test-folder/docs
44+
45+
# Create various files
46+
echo "<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Test Page</h1></body></html>" > test-folder/index.html
47+
echo "body { font-family: Arial, sans-serif; }" > test-folder/assets/css/style.css
48+
echo "console.log('Hello from test!');" > test-folder/assets/js/script.js
49+
echo "# Test Documentation" > test-folder/docs/README.md
50+
echo "This is a test file created at $(date)" > test-folder/docs/info.txt
51+
52+
# Create a nested structure
53+
mkdir -p test-folder/assets/images
54+
echo "Fake image data" > test-folder/assets/images/logo.txt
55+
56+
echo "Created test folder structure:"
57+
find test-folder -type f
58+
59+
- name: Upload entire folder to Vercel Blob
60+
uses: ./
61+
with:
62+
source: "test-folder/"
63+
destination: "github-actions-test/folder-upload-${{ github.run_number }}"
64+
read-write-token: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
65+
66+
- name: Cleanup
67+
run: rm -rf test-folder
68+
69+
test-results:
70+
runs-on: ubuntu-latest
71+
name: Test Results Summary
72+
needs: [test-single-file, test-folder-upload]
73+
if: always()
74+
75+
steps:
76+
- name: Summary
77+
run: |
78+
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
79+
echo "" >> $GITHUB_STEP_SUMMARY
80+
echo "### Single File Upload" >> $GITHUB_STEP_SUMMARY
81+
if [ "${{ needs.test-single-file.result }}" == "success" ]; then
82+
echo "✅ **PASSED** - Single file upload completed successfully" >> $GITHUB_STEP_SUMMARY
83+
else
84+
echo "❌ **FAILED** - Single file upload failed" >> $GITHUB_STEP_SUMMARY
85+
fi
86+
echo "" >> $GITHUB_STEP_SUMMARY
87+
echo "### Folder Upload" >> $GITHUB_STEP_SUMMARY
88+
if [ "${{ needs.test-folder-upload.result }}" == "success" ]; then
89+
echo "✅ **PASSED** - Folder upload completed successfully" >> $GITHUB_STEP_SUMMARY
90+
else
91+
echo "❌ **FAILED** - Folder upload failed" >> $GITHUB_STEP_SUMMARY
92+
fi
93+
echo "" >> $GITHUB_STEP_SUMMARY
94+
echo "### Access Files" >> $GITHUB_STEP_SUMMARY
95+
echo "Check your [Vercel Dashboard](https://vercel.com/dashboard) under Storage → Blob to access the uploaded files." >> $GITHUB_STEP_SUMMARY
96+
echo "" >> $GITHUB_STEP_SUMMARY
97+
echo "**Files uploaded:**" >> $GITHUB_STEP_SUMMARY
98+
echo "- \`github-actions-test/single-file-${{ github.run_number }}.txt\`" >> $GITHUB_STEP_SUMMARY
99+
echo "- \`github-actions-test/folder-upload-${{ github.run_number }}/\` (folder with multiple files)" >> $GITHUB_STEP_SUMMARY

README.md

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
# Upload to Vercel Blob
22

3-
This GitHub Action allows you to upload files to Vercel Blob storage by specifying a source file and destination path. It provides an easy way to manage blob storage in your Vercel projects through GitHub Actions workflows.
3+
This GitHub Action allows you to upload files or entire folders to Vercel Blob storage by specifying a source path and destination path. It provides an easy way to manage blob storage in your Vercel projects through GitHub Actions workflows.
44

55
## Inputs
66

77
### `source`
88

9-
**Required** The source path of the file you want to upload to Vercel Blob storage.
9+
**Required** The source path of the file or folder you want to upload to Vercel Blob storage.
10+
11+
- **File**: Upload a single file
12+
- **Folder**: Upload all files within the folder (including subdirectories)
1013

1114
### `destination`
1215

13-
**Required** The destination path where the file should be stored in Vercel Blob storage.
16+
**Required** The destination path where the file(s) should be stored in Vercel Blob storage.
17+
18+
- **For files**: The exact destination path for the file
19+
- **For folders**: The base path where all files will be uploaded (maintaining folder structure)
1420

1521
### `read-write-token`
1622

1723
**Required** Your Vercel Blob read-write token (`BLOB_READ_WRITE_TOKEN`). This should be stored as a GitHub secret for security.
1824

1925
## Outputs
2026

21-
### `url`
22-
23-
The URL of the uploaded blob file.
27+
This action does not provide any outputs for privacy and security reasons. To access your uploaded files, check your Vercel dashboard under Storage → Blob.
2428

2529
## Environment Variables
2630

@@ -33,34 +37,38 @@ This action requires a Vercel Blob read-write token. The action will automatical
3337

3438
## Usage
3539

36-
To use this action in your workflow, add it as a step in your GitHub Actions workflow file:
40+
### Single File Upload
41+
42+
Upload a single file to Vercel Blob storage:
3743

3844
```yaml
39-
name: Upload to Vercel Blob
45+
name: Upload Single File
4046

41-
on: [push]
47+
on:
48+
push:
49+
branches: [main]
4250

4351
jobs:
44-
upload_blob:
52+
upload-file:
4553
runs-on: ubuntu-latest
4654
steps:
4755
- name: Checkout code
4856
uses: actions/checkout@v4
4957

5058
- name: Upload file to Vercel Blob
51-
uses: dartilesm/vercel-blob-action@v1
59+
uses: dartilesm/vercel-blob-action@v2
5260
with:
53-
source: "path/to/your/file.txt"
61+
source: "path/to/file.txt"
5462
destination: "uploads/file.txt"
5563
read-write-token: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
5664
```
5765
58-
## Example
66+
### Folder Upload
5967
60-
Here's a complete example that uploads a build artifact to Vercel Blob storage:
68+
Upload an entire folder (including subdirectories) to Vercel Blob storage:
6169
6270
```yaml
63-
name: Build and Upload
71+
name: Upload Build Folder
6472

6573
on:
6674
push:
@@ -76,19 +84,25 @@ jobs:
7684
- name: Build project
7785
run: |
7886
# Your build commands here
79-
echo "Built file" > dist/output.txt
87+
mkdir -p dist/assets
88+
echo "Main file" > dist/index.html
89+
echo "Stylesheet" > dist/assets/style.css
90+
echo "Script" > dist/assets/script.js
8091
81-
- name: Upload to Vercel Blob
82-
uses: dartilesm/vercel-blob-action@v1
92+
- name: Upload entire build folder to Vercel Blob
93+
uses: dartilesm/vercel-blob-action@v2
8394
with:
84-
source: "dist/output.txt"
85-
destination: "builds/output-${{ github.sha }}.txt"
95+
source: "dist/"
96+
destination: "builds/${{ github.sha }}"
8697
read-write-token: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
87-
88-
- name: Display blob URL
89-
run: echo "File uploaded to ${{ steps.upload.outputs.url }}"
9098
```
9199
100+
This will upload all files maintaining the folder structure:
101+
102+
- `dist/index.html` → `builds/{sha}/index.html`
103+
- `dist/assets/style.css` → `builds/{sha}/assets/style.css`
104+
- `dist/assets/script.js` → `builds/{sha}/assets/script.js`
105+
92106
## Setting up the Token
93107

94108
1. **Get your Vercel Blob token:**

action.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ author: "dartilesm"
99

1010
# Input parameters that users can provide when using this action
1111
inputs:
12-
# Source file path - the file to be uploaded
12+
# Source file or folder path - the file or folder to be uploaded
1313
source:
14-
description: "The source path for the blob file"
14+
description: "The source path for the blob file or folder"
1515
required: true
1616

1717
# Destination path in Vercel Blob storage
1818
destination:
19-
description: "The destination path for the blob in Vercel Blob storage"
19+
description: "The destination path for the blob in Vercel Blob storage (for folders, this will be the base path)"
2020
required: true
2121

2222
# Vercel Blob read-write token for authentication

dist/index.js

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27975,6 +27975,8 @@ var __webpack_exports__ = {};
2797527975
var core = __nccwpck_require__(7484);
2797627976
// EXTERNAL MODULE: external "fs"
2797727977
var external_fs_ = __nccwpck_require__(9896);
27978+
// EXTERNAL MODULE: external "path"
27979+
var external_path_ = __nccwpck_require__(6928);
2797827980
// EXTERNAL MODULE: ./node_modules/undici/index.js
2797927981
var undici = __nccwpck_require__(6752);
2798027982
// EXTERNAL MODULE: ./node_modules/async-retry/lib/index.js
@@ -28994,10 +28996,45 @@ var dist_completeMultipartUpload = createCompleteMultipartUploadMethod({
2899428996
// Import required modules
2899528997
// GitHub Actions toolkit for inputs, outputs, and logging
2899628998
// Node.js file system module for file operations
28999+
// Node.js path module for path operations
2899729000
// Vercel Blob SDK for uploading files
29001+
/**
29002+
* Recursively gets all files in a directory
29003+
* @param dirPath - Directory path to scan
29004+
* @param fileList - Array to store file paths (used for recursion)
29005+
* @returns Array of file paths
29006+
*/
29007+
function getAllFiles(dirPath, fileList = []) {
29008+
const files = external_fs_.readdirSync(dirPath);
29009+
files.forEach(file => {
29010+
const filePath = external_path_.join(dirPath, file);
29011+
const stat = external_fs_.statSync(filePath);
29012+
if (stat.isDirectory()) {
29013+
// Recursively get files from subdirectories
29014+
getAllFiles(filePath, fileList);
29015+
}
29016+
else {
29017+
// Add file to the list
29018+
fileList.push(filePath);
29019+
}
29020+
});
29021+
return fileList;
29022+
}
29023+
/**
29024+
* Uploads a single file to Vercel Blob storage
29025+
* @param filePath - Path to the file to upload
29026+
* @param destinationPath - Destination path in Vercel Blob storage
29027+
* @returns Promise resolving to the upload result
29028+
*/
29029+
async function uploadFile(filePath, destinationPath) {
29030+
const fileStream = external_fs_.createReadStream(filePath);
29031+
await put(destinationPath, fileStream, {
29032+
access: 'public'
29033+
});
29034+
}
2899829035
/**
2899929036
* Main function that executes the action
29000-
* Handles file upload to Vercel Blob storage with proper error handling
29037+
* Handles file or folder upload to Vercel Blob storage with proper error handling
2900129038
*/
2900229039
async function run() {
2900329040
try {
@@ -29011,26 +29048,45 @@ async function run() {
2901129048
if (!token) {
2901229049
throw new Error('Vercel Blob read-write token is required. Please set the read-write-token input with your BLOB_READ_WRITE_TOKEN.');
2901329050
}
29014-
// Check if the source file exists before attempting upload
29051+
// Check if the source path exists before attempting upload
2901529052
// This prevents unnecessary API calls and provides clear error messages
2901629053
if (!external_fs_.existsSync(sourcePath)) {
29017-
throw new Error(`Source file does not exist: ${sourcePath}`);
29054+
throw new Error(`Source path does not exist: ${sourcePath}`);
2901829055
}
29019-
// Log the upload operation for debugging and monitoring
29020-
core.info(`Uploading file from ${sourcePath} to ${destinationPath}`);
29021-
// Create a readable stream from the source file
29022-
// Streaming is more memory-efficient for large files
29023-
const fileStream = external_fs_.createReadStream(sourcePath);
2902429056
// Set the token as an environment variable for the Vercel Blob SDK
2902529057
// The SDK automatically reads BLOB_READ_WRITE_TOKEN from environment variables
2902629058
process.env.BLOB_READ_WRITE_TOKEN = token;
29027-
// Upload the file to Vercel Blob storage
29028-
// The put() function handles the actual upload and returns metadata about the uploaded blob
29029-
const result = await put(destinationPath, fileStream, {
29030-
access: 'public'
29031-
});
29032-
// Log successful upload without exposing the URL
29033-
core.info('File uploaded successfully to Vercel Blob. Check your Vercel dashboard to access the file.');
29059+
const stat = external_fs_.statSync(sourcePath);
29060+
// Handle single file upload
29061+
if (stat.isFile()) {
29062+
core.info(`Uploading file from ${sourcePath} to ${destinationPath}`);
29063+
await uploadFile(sourcePath, destinationPath);
29064+
core.info('File uploaded successfully to Vercel Blob. Check your Vercel dashboard to access the file.');
29065+
return;
29066+
}
29067+
// Handle folder upload
29068+
if (stat.isDirectory()) {
29069+
core.info(`Uploading folder from ${sourcePath} to ${destinationPath}`);
29070+
const files = getAllFiles(sourcePath);
29071+
if (files.length === 0) {
29072+
core.warning('No files found in the specified directory.');
29073+
return;
29074+
}
29075+
core.info(`Found ${files.length} files to upload`);
29076+
// Upload each file
29077+
for (const filePath of files) {
29078+
// Calculate relative path from source directory
29079+
const relativePath = external_path_.relative(sourcePath, filePath);
29080+
// Create destination path by joining the destination with the relative path
29081+
const fileDestination = external_path_.posix.join(destinationPath, relativePath);
29082+
core.info(`Uploading: ${relativePath} → ${fileDestination}`);
29083+
await uploadFile(filePath, fileDestination);
29084+
}
29085+
core.info(`Successfully uploaded ${files.length} files to Vercel Blob. Check your Vercel dashboard to access the files.`);
29086+
return;
29087+
}
29088+
// If neither file nor directory, throw error
29089+
throw new Error(`Source path is neither a file nor a directory: ${sourcePath}`);
2903429090
}
2903529091
catch (error) {
2903629092
// Handle any errors that occur during the upload process

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)