Upload via API

Buat key di admin, tentukan folder aplikasi dan simpan secret di environment aplikasi Anda.

Panduan penggunaan

  1. Buka halaman admin, login, lalu pilih Buat API key.
  2. Isi nama aplikasi dan folder tujuan, misalnya APP_KLINIK. File masuk ke WebFileApiSentral/APP_KLINIK/[id-key]/[hash]/.
  3. Pilih tanpa batas domain, atau aktifkan whitelist dan isi origin lengkap, misalnya https://klinik.example.com.
  4. Salin secret yang ditampilkan sekali. Simpan sebagai API_KEY pada environment backend aplikasi Anda.
  5. Isi API_BASE_URL dengan https://cdn.sentralmedika.co.id.
  6. Mulai sesi, kirim byte file dalam potongan berurutan, lalu finalisasi. File siap dibagikan setelah finalisasi mengembalikan status 201.

API key menentukan folder penyimpanan. Tidak perlu mengirim parameter folder atau password upload manual.

API key dan origin aplikasi pada contoh ini merupakan placeholder. Ganti dengan konfigurasi aplikasi Anda. Ukuran file harus dihitung dalam byte dari file asli, bukan KB/MB atau ukuran perkiraan.

Autentikasi

Authorization: Bearer fv_ID_SECRET

Alternatif: header X-API-Key. Gunakan key yang sama pada semua tahap.

Mulai upload

POST /api/upload/start
Content-Type: application/json
Authorization: Bearer API_KEY

{"name":"laporan.pdf","size":12345}

Response berisi session terenkripsi dan chunkSize (3.125 MiB).

Kirim potongan file berurutan

PUT /api/upload/chunk
Authorization: Bearer API_KEY
X-Upload-Session: SESSION
X-Upload-Offset: 0
Content-Type: application/octet-stream

[byte file]

Offset dimulai dari 0 dan bertambah sesuai chunkSize dari respons awal. Default chunkSize adalah 3276800 byte. Setiap chunk harus tepat sebesar chunkSize, kecuali yang terakhir. Contoh file 7000000 byte: offset 0 (3276800 byte), offset 3276800 (3276800 byte), offset 6553600 (446400 byte).

Kirim potongan satu per satu setelah request sebelumnya berhasil. Body memakai byte mentah, bukan multipart/form-data. Jangan mengubah file selama upload. API key harus sama pada semua tahap; header sesi dari start dipakai pada chunk dan finish. Jika upload gagal, mulai sesi baru. Rotasi key membatalkan kelanjutan sesi dengan secret lama.

Finalisasi

POST /api/upload/finish
Authorization: Bearer API_KEY
X-Upload-Session: SESSION

Response 201 berisi hash, SHA-256 konten, nama, ukuran, folder aplikasi, ID key, URL preview dan download relatif pada domain layanan.

Folder penyimpanan

WebFileApiSentral/
  PUBLIC/[hash]/file.ext
  APP_KLINIK/[id-key]/[hash]/file.ext
  APP_HRD/[id-key]/[hash]/file.ext
  _keys/[id-key].json
  _files/[hash]/metadata.json

Folder ditentukan admin, bukan client upload. Rotasi key mempertahankan folder dan file. Folder key tidak dapat diubah; buat key baru untuk aplikasi/folder baru.

Whitelist domain

Whitelist memakai origin persis, termasuk scheme dan port. Contoh: https://klinik.example.com. Key dengan whitelist wajib mengirim header Origin yang cocok pada setiap tahap upload. CORS mengizinkan browser dari origin tersebut.

Origin dapat dipalsukan oleh client di luar browser. Whitelist adalah pembatasan browser, bukan bukti identitas server. Simpan API key pada backend aplikasi; key di JavaScript publik bisa dibaca pengguna.

Contoh Node.js

Client lengkap tersedia di examples/upload.mjs pada repository. Client membaca file per chunk untuk video atau dokumen besar. Jalankan dari folder proyek dengan Node.js yang menyediakan fetch bawaan.

Windows / PowerShell

$env:API_BASE_URL = 'https://cdn.sentralmedika.co.id'
$env:API_KEY = 'GANTI_DENGAN_KEY_DARI_ADMIN'
# Hanya jika key memakai whitelist:
$env:API_ORIGIN = 'https://klinik.example.com'

node examples/upload.mjs 'C:\dokumen\laporan.pdf'

Linux / macOS

export API_BASE_URL='https://cdn.sentralmedika.co.id'
export API_KEY='GANTI_DENGAN_KEY_DARI_ADMIN'
# Hanya jika key memakai whitelist:
export API_ORIGIN='https://klinik.example.com'

node examples/upload.mjs './laporan.pdf'

Untuk key tanpa whitelist, API_ORIGIN boleh tidak diisi. Hapus nilai lama pada PowerShell dengan Remove-Item Env:API_ORIGIN -ErrorAction SilentlyContinue atau pada Bash dengan unset API_ORIGIN.

Program menampilkan progres dan hasil JSON dengan URL preview serta download absolut. Endpoint API sendiri mengembalikan URL relatif; client contoh menggabungkannya dengan domain layanan.

Lihat client Node.js lengkap
import { open, stat } from 'node:fs/promises';
import { basename } from 'node:path';

const file = process.argv[2];
const base = process.env.API_BASE_URL;
const key = process.env.API_KEY;
if (!file || !base || !key) {
  throw new Error('Isi API_BASE_URL, API_KEY, lalu node examples/upload.mjs /path/file.pdf');
}

const headers = {
  Authorization: 'Bearer ' + key,
  ...(process.env.API_ORIGIN ? { Origin: process.env.API_ORIGIN } : {})
};

async function request(path, options = {}) {
  const response = await fetch(new URL(path, base), {
    ...options,
    headers: { ...headers, ...options.headers }
  });
  const data = await response.json();
  if (!response.ok) throw new Error(data.error || 'Request gagal');
  return data;
}

const info = await stat(file);
const start = await request('/api/upload/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: basename(file), size: info.size })
});
const handle = await open(file, 'r');
try {
  for (let offset = 0; offset < info.size; offset += start.chunkSize) {
    const bytes = Buffer.alloc(Math.min(start.chunkSize, info.size - offset));
    let read = 0;
    while (read < bytes.length) {
      const part = await handle.read(bytes, read, bytes.length - read, offset + read);
      if (!part.bytesRead) throw new Error('File berubah saat dibaca');
      read += part.bytesRead;
    }
    await request('/api/upload/chunk', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/octet-stream',
        'X-Upload-Session': start.session,
        'X-Upload-Offset': String(offset)
      },
      body: bytes
    });
    console.log(Math.round(Math.min(offset + start.chunkSize, info.size) / info.size * 100) + '%');
  }
} finally {
  await handle.close();
}

const result = await request('/api/upload/finish', {
  method: 'POST',
  headers: { 'X-Upload-Session': start.session }
});
console.log(JSON.stringify({
  ...result,
  url: new URL(result.url, base).href,
  downloadUrl: new URL(result.downloadUrl, base).href
}, null, 2));

Contoh cURL

Contoh Bash ini memakai curl dan jq untuk file maksimal satu chunk, 3.125 MiB. Untuk file besar gunakan Node.js atau PHP yang membagi file secara otomatis. Jalankan dari direktori file Anda.

# Bash/Linux: perlu curl dan jq. Contoh satu chunk.
# Gunakan Node.js atau PHP untuk file besar.
set -e
BASE_URL='https://cdn.sentralmedika.co.id'
API_KEY='GANTI_DENGAN_KEY_DARI_ADMIN'
API_ORIGIN='' # Isi origin whitelist jika key dibatasi
FILE='./laporan.pdf'

SIZE=$(wc -c < "$FILE" | tr -d '[:space:]')
if [ "$SIZE" -gt 3276800 ]; then
  echo 'Gunakan client Node.js/PHP untuk file di atas 3.125 MiB.'
  exit 1
fi
PAYLOAD=$(jq -cn --arg name "$(basename "$FILE")" \
  --argjson size "$SIZE" '{name:$name,size:$size}')

START=$(curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $API_KEY" -H "Origin: $API_ORIGIN" \
  -H 'Content-Type: application/json' \
  --data-binary "$PAYLOAD" "$BASE_URL/api/upload/start")
SESSION=$(printf '%s' "$START" | jq -er '.session')

curl --fail-with-body --silent --show-error \
  -X PUT -H "Authorization: Bearer $API_KEY" \
  -H "Origin: $API_ORIGIN" \
  -H "X-Upload-Session: $SESSION" -H 'X-Upload-Offset: 0' \
  -H 'Content-Type: application/octet-stream' \
  --data-binary "@$FILE" "$BASE_URL/api/upload/chunk"

curl --fail-with-body --silent --show-error \
  -X POST -H "Authorization: Bearer $API_KEY" \
  -H "Origin: $API_ORIGIN" \
  -H "X-Upload-Session: $SESSION" \
  "$BASE_URL/api/upload/finish"

Body chunk adalah byte file mentah dengan --data-binary, bukan multipart/form-data atau string base64. Di PowerShell gunakan contoh Node.js karena sintaks Bash berbeda.

Referensi opsi: dokumentasi resmi curl.

Contoh PHP

Helper lengkap tersedia di examples/upload.php. PHP memerlukan ekstensi cURL. Helper membaca file per chunk dan mengembalikan URL preview serta unduhan absolut.

Menjalankan dari terminal

$env:API_BASE_URL = 'https://cdn.sentralmedika.co.id'
$env:API_KEY = 'GANTI_DENGAN_KEY_DARI_ADMIN'
# Hanya jika key memakai whitelist:
$env:API_ORIGIN = 'https://klinik.example.com'

php examples/upload.php 'C:\dokumen\laporan.pdf'

Memakai helper dari aplikasi PHP

<?php
require __DIR__ . '/examples/upload.php';

$result = uploadFileViaApi(
    __DIR__ . '/dokumen/laporan.pdf',
    getenv('API_BASE_URL'),
    getenv('API_KEY'),
    getenv('API_ORIGIN') ?: null
);

echo $result['url'];         // URL halaman preview
echo $result['downloadUrl']; // URL unduhan asli
Lihat kode helper PHP lengkap
<?php
declare(strict_types=1);

function requestUploadApi(
    string $baseUrl, string $apiKey, ?string $origin,
    string $path, string $method, ?string $body = null,
    array $extraHeaders = []
): array {
    $headers = array_merge(['Authorization: Bearer ' . $apiKey], $extraHeaders);
    if ($origin !== null && $origin !== '') {
        $headers[] = 'Origin: ' . $origin;
    }
    $ch = curl_init(rtrim($baseUrl, '/') . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CONNECTTIMEOUT => 15,
        CURLOPT_TIMEOUT => 330,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $response = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    unset($ch);
    if ($response === false) {
        throw new RuntimeException('Koneksi API gagal: ' . $error);
    }
    $data = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
    if ($status < 200 || $status >= 300) {
        throw new RuntimeException('HTTP ' . $status . ': ' . ($data['error'] ?? 'Upload gagal.'));
    }
    return $data;
}

function uploadFileViaApi(
    string $filePath, string $baseUrl,
    string $apiKey, ?string $origin = null
): array {
    if (!is_file($filePath) || !is_readable($filePath)) {
        throw new RuntimeException('File tidak ditemukan atau tidak dapat dibaca.');
    }
    $size = filesize($filePath);
    if ($size === false || $size < 1) {
        throw new RuntimeException('File kosong atau ukuran tidak dapat dibaca.');
    }
    $start = requestUploadApi(
        $baseUrl, $apiKey, $origin, '/api/upload/start', 'POST',
        json_encode(['name' => basename($filePath), 'size' => $size], JSON_THROW_ON_ERROR),
        ['Content-Type: application/json']
    );
    $chunkSize = (int) ($start['chunkSize'] ?? 0);
    $session = $start['session'] ?? '';
    if ($chunkSize < 1 || $session === '') {
        throw new RuntimeException('Respons sesi upload tidak valid.');
    }
    $handle = fopen($filePath, 'rb');
    if ($handle === false) {
        throw new RuntimeException('Tidak dapat membuka file.');
    }
    try {
        for ($offset = 0; $offset < $size; $offset += $chunkSize) {
            $length = min($chunkSize, $size - $offset);
            $bytes = '';
            while (strlen($bytes) < $length) {
                $part = fread($handle, $length - strlen($bytes));
                if ($part === false || $part === '') {
                    throw new RuntimeException('File berubah atau gagal dibaca saat upload.');
                }
                $bytes .= $part;
            }
            requestUploadApi(
                $baseUrl, $apiKey, $origin, '/api/upload/chunk', 'PUT', $bytes,
                ['Content-Type: application/octet-stream',
                 'X-Upload-Session: ' . $session,
                 'X-Upload-Offset: ' . $offset]
            );
        }
    } finally {
        fclose($handle);
    }
    $result = requestUploadApi(
        $baseUrl, $apiKey, $origin, '/api/upload/finish', 'POST', null,
        ['X-Upload-Session: ' . $session]
    );
    $result['url'] = rtrim($baseUrl, '/') . $result['url'];
    $result['downloadUrl'] = rtrim($baseUrl, '/') . $result['downloadUrl'];
    return $result;
}

if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    try {
        $baseUrl = getenv('API_BASE_URL') ?: '';
        $apiKey = getenv('API_KEY') ?: '';
        $filePath = $argv[1] ?? '';
        if ($baseUrl === '' || $apiKey === '' || $filePath === '') {
            throw new RuntimeException('Isi API_BASE_URL, API_KEY, lalu php examples/upload.php /path/file.pdf');
        }
        $result = uploadFileViaApi($filePath, $baseUrl, $apiKey, getenv('API_ORIGIN') ?: null);
        echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL;
    } catch (Throwable $error) {
        fwrite(STDERR, $error->getMessage() . PHP_EOL);
        exit(1);
    }
}

Sesuaikan path helper dan file dengan struktur aplikasi. Secret berada di environment server, bukan pada halaman HTML.

Memakai hasil upload

Ilustrasi field respons finalisasi berikut memakai placeholder, bukan file nyata.

{
  "hash": "<hash-64-karakter>",
  "name": "laporan.pdf",
  "size": 12345,
  "mime": "application/pdf",
  "sha256": "<sha256-konten>",
  "appFolder": "APP_KLINIK",
  "apiKeyId": "<id-key>",
  "url": "/f/<hash-64-karakter>",
  "downloadUrl": "/api/files/<hash-64-karakter>/content?download=1"
}

Simpan hash, url dan downloadUrl pada database aplikasi Anda. SHA-256 konten tersedia pada field sha256 untuk pengecekan integritas.

const previewUrl = new URL(result.url, API_BASE_URL).href;
const downloadUrl = new URL(result.downloadUrl, API_BASE_URL).href;

Arahkan pengguna ke halaman preview. PDF dan dokumen Office yang didukung ditampilkan pada viewer aplikasi; tidak perlu menyimpan URL Microsoft.

Siapa pun yang memiliki URL hash dapat membuka file. Whitelist membatasi upload, bukan akses baca. Endpoint konten hanya mengizinkan frame dari domain layanan sendiri; dari aplikasi lain gunakan link menuju halaman preview.

Respons error

  • 400: periksa nama/ekstensi, ukuran byte, offset dan panjang chunk.
  • 401: key salah, nonaktif/kedaluwarsa, atau sesi upload kedaluwarsa.
  • 403: Origin tidak masuk whitelist, hilang, atau sesi memakai key lain.
  • 413: request terlalu besar; gunakan chunkSize dari start.
  • 502: Microsoft Graph/upload gagal; periksa konfigurasi dan mulai upload baru.
  • 503: environment layanan belum lengkap.

Respons error aplikasi berbentuk {"error":"pesan"}. Jika browser hanya menampilkan error CORS, periksa origin dan status request pada Network; respons key/origin yang ditolak dapat tidak memiliki header CORS.

Contoh client Node.js dan PHP tersedia di folder examples pada repository. Preview tetap memakai domain layanan; pengguna dengan link hash dapat membaca file tanpa API key.