新增桶权限选择
This commit is contained in:
@@ -5,8 +5,12 @@ HOST=0.0.0.0
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your_access_key
|
||||
AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||
S3_BUCKET_ADDRESS=https://s3.example.com/your_bucket_name
|
||||
S3_BUCKET_NAME=your_s3_bucket_name
|
||||
|
||||
# For MinIO or other S3-compatible storage
|
||||
S3_ENDPOINT=http://127.0.0.1:9000
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# Application Title
|
||||
APP_TITLE=S3 Media Transcoder
|
||||
|
||||
@@ -129,6 +129,24 @@ header p {
|
||||
box-shadow: 0 20px 40px rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
|
||||
.top-banner {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 16px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top-banner.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -157,6 +175,39 @@ header p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.credentials-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.credential-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 180px;
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.credential-field label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.credential-field input {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.credential-field input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.codec-panel label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>S3 Media <span>Transcoder</span></h1>
|
||||
<p>Seamlessly transcode videos from AWS S3 to MP4 for browser playback</p>
|
||||
</header>
|
||||
<div id="top-banner" class="top-banner hidden"></div>
|
||||
|
||||
<main class="dashboard">
|
||||
<section class="video-list-section glass-panel">
|
||||
@@ -35,6 +32,16 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credentials-panel">
|
||||
<div class="credential-field">
|
||||
<label for="s3-username">Username</label>
|
||||
<input id="s3-username" type="text" placeholder="Access key" autocomplete="username">
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<label for="s3-password">Password</label>
|
||||
<input id="s3-password" type="password" placeholder="Secret key" autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="codec-panel">
|
||||
<label for="codec-select">编码方式:</label>
|
||||
<select id="codec-select">
|
||||
|
||||
@@ -3,6 +3,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const resetCacheBtn = document.getElementById('reset-cache-btn');
|
||||
const usernameInput = document.getElementById('s3-username');
|
||||
const passwordInput = document.getElementById('s3-password');
|
||||
const codecSelect = document.getElementById('codec-select');
|
||||
const encoderSelect = document.getElementById('encoder-select');
|
||||
const playerOverlay = document.getElementById('player-overlay');
|
||||
@@ -15,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const progressInfo = document.getElementById('progress-info');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const topBanner = document.getElementById('top-banner');
|
||||
|
||||
let currentPollInterval = null;
|
||||
let selectedKey = null;
|
||||
@@ -51,6 +54,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!topBanner) return;
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
if (!res.ok) throw new Error('Failed to load config');
|
||||
const data = await res.json();
|
||||
const title = data.title || 'S3 Media Transcoder';
|
||||
topBanner.textContent = title;
|
||||
topBanner.classList.remove('hidden');
|
||||
document.title = title;
|
||||
} catch (err) {
|
||||
console.error('Config load failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const connectWebSocket = () => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${protocol}://${window.location.host}`);
|
||||
@@ -90,6 +108,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
progressFill.style.width = '0%';
|
||||
};
|
||||
|
||||
const getS3AuthHeaders = () => {
|
||||
const headers = {};
|
||||
const username = usernameInput?.value?.trim();
|
||||
const password = passwordInput?.value || '';
|
||||
if (username) headers['X-S3-Username'] = username;
|
||||
if (password) headers['X-S3-Password'] = password;
|
||||
return headers;
|
||||
};
|
||||
|
||||
const getS3AuthPayload = () => ({
|
||||
username: usernameInput?.value?.trim() || '',
|
||||
password: passwordInput?.value || ''
|
||||
});
|
||||
|
||||
const resetCache = async () => {
|
||||
if (!resetCacheBtn) return;
|
||||
resetCacheBtn.disabled = true;
|
||||
@@ -149,7 +181,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
videoListEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/videos');
|
||||
const res = await fetch('/api/videos', { headers: getS3AuthHeaders() });
|
||||
if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.');
|
||||
|
||||
const data = await res.json();
|
||||
@@ -296,10 +328,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const codec = codecSelect?.value || 'h264';
|
||||
const encoder = encoderSelect?.value || 'software';
|
||||
const authPayload = getS3AuthPayload();
|
||||
const res = await fetch('/api/transcode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ key: selectedKey, codec, encoder })
|
||||
headers: { 'Content-Type': 'application/json', ...getS3AuthHeaders() },
|
||||
body: JSON.stringify({ key: selectedKey, codec, encoder, ...authPayload })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
@@ -374,5 +407,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Connect WebSocket and initial load
|
||||
connectWebSocket();
|
||||
loadConfig();
|
||||
fetchVideos();
|
||||
});
|
||||
|
||||
66
server.js
66
server.js
@@ -20,18 +20,44 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Configure AWS S3 Client
|
||||
const s3Client = new S3Client({
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
});
|
||||
const rawBucketAddress = process.env.S3_BUCKET_ADDRESS || process.env.S3_BUCKET_NAME || '';
|
||||
let BUCKET_NAME = rawBucketAddress;
|
||||
|
||||
const parsedBucketUrl = rawBucketAddress.includes('://') ? (() => {
|
||||
try {
|
||||
const parsed = new URL(rawBucketAddress);
|
||||
const name = parsed.pathname.replace(/^\/+/, '');
|
||||
if (name) {
|
||||
BUCKET_NAME = name;
|
||||
}
|
||||
return parsed.origin;
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
})() : undefined;
|
||||
|
||||
const defaultS3ClientConfig = {
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
endpoint: process.env.S3_ENDPOINT || parsedBucketUrl,
|
||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true'
|
||||
};
|
||||
|
||||
const createS3Client = (credentials) => {
|
||||
const clientConfig = { ...defaultS3ClientConfig };
|
||||
if (credentials && credentials.username && credentials.password) {
|
||||
clientConfig.credentials = {
|
||||
accessKeyId: credentials.username,
|
||||
secretAccessKey: credentials.password
|
||||
};
|
||||
} else if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
clientConfig.credentials = {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||
};
|
||||
}
|
||||
return new S3Client(clientConfig);
|
||||
};
|
||||
|
||||
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
||||
const progressMap = {};
|
||||
const wsSubscriptions = new Map();
|
||||
|
||||
@@ -84,6 +110,15 @@ const shouldRetryWithSoftware = (message) => {
|
||||
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument/i.test(message);
|
||||
};
|
||||
|
||||
const extractS3Credentials = (req) => {
|
||||
const username = req.headers['x-s3-username'] || req.body?.username || '';
|
||||
const password = req.headers['x-s3-password'] || req.body?.password || '';
|
||||
return {
|
||||
username: typeof username === 'string' ? username.trim() : '',
|
||||
password: typeof password === 'string' ? password : ''
|
||||
};
|
||||
};
|
||||
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
@@ -134,6 +169,8 @@ app.get('/api/videos', async (req, res) => {
|
||||
return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' });
|
||||
}
|
||||
const allObjects = [];
|
||||
const auth = extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
let continuationToken;
|
||||
|
||||
do {
|
||||
@@ -168,6 +205,11 @@ app.get('/api/videos', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/config', (req, res) => {
|
||||
const title = process.env.APP_TITLE || 'S3 Media Transcoder';
|
||||
res.json({ title });
|
||||
});
|
||||
|
||||
app.post('/api/reset-cache', (req, res) => {
|
||||
try {
|
||||
clearMp4Cache();
|
||||
@@ -215,6 +257,8 @@ app.post('/api/transcode', async (req, res) => {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// Get S3 stream
|
||||
const auth = extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: key
|
||||
|
||||
Reference in New Issue
Block a user