📡 Документация по API эндпоинта upload.php v2.20.2

Содержание
  1. 🔍 1. Общая информация
  2. 🔐 2. Аутентификация и авторизация
  3. 📤 3. Базовые примеры загрузки файлов
  4. 🚀 4. Расширенные возможности
  5. ⚠️ 5. Обработка ответов и ошибок
  6. 📱 6. Особенности работы с данным API из Android приложений
  7. ⚙️ 7. Тонкая настройка и редкие случаи
  8. 📋 8. Приложения
  9. 🎯 Заключение

🔍 1. Общая информация

1.1 Назначение эндпоинта

/api/v2/upload.php — специализированный эндпоинт для асинхронной загрузки файлов в систему BatteryWizard. Поддерживает комплексные сценарии загрузки, включая основной файл данных и сопутствующие изображения с привязкой к типам контента.

1.2 Основные возможности

  • 📤 Загрузка основного файла (форматы: CBE, RTA, BSL, DAT, и другие)
  • 🖼️ Сопутствующие изображения (JPG, PNG, GIF, WebP, BMP) с привязкой к ContentType
  • 🔒 Многоуровневая безопасность (IP-фильтрация, rate-limiting, проверка контента)
  • 📊 Детальное логирование и отладка в test-режиме
  • 💾 Автоматическое разрешение коллизий имён файлов
  • 🔍 Валидация содержимого (сигнатуры файлов, MIME-типы)
  • 🔄 Транзакционная обработка (atomic-операции с rollback)

1.3 Архитектурные ограничения

Максимальный размер основного файла: 100 MB (конфигурируется)
Максимальный размер изображения:    10 MB (конфигурируется)
Минимальный размер файла:           1 байт
Требуется один основной файл:       Обязательно ровно 1
Количество изображений:             От 0 до N (ограничено лимитами пользователя)

🔐 2. Аутентификация и авторизация

2.1 Методы передачи токена

Эндпоинт поддерживает три способа передачи JWT-токена (в порядке приоритета):

2.1.1 HTTP Header (рекомендуемый)

Authorization: Bearer bdf380fc8f1234567890abcdef123456

2.1.2 GET/POST параметр

POST /api/v2/upload.php?token=bdf380fc8f1234567890abcdef123456

2.1.3 Multipart Form Data

<input type="hidden" name="token" value="bdf380fc8f1234567890abcdef123456">

2.2 Лимиты пользователя

Каждый пользователь имеет индивидуальные лимиты:

ЛимитЗначение по умолчаниюОписание
Daily Upload Limit500 файловМаксимальное количество загрузок в сутки
Trust Score100 (макс)Динамическая оценка доверия
AllowBan1 (разрешено)Возможность блокировки системы безопасности

Важно: При достижении лимита возвращается HTTP 429 с сообщением «Daily upload limit exceeded».

📤 3. Базовые примеры загрузки файлов

3.1 Простейший запрос (один файл)

cURL

curl -X POST "https://api.batterywizard.ru/v2/upload.php" \
  -H "Authorization: Bearer bdf380fc8f1234567890abcdef123456" \
  -F "upload_file=@test_file.rta" \
  -F "filename=test_file.rta" \
  -F "version=1.0"

Пример успешного ответа (HTTP 200)

{
  "success": true,
  "code": 200,
  "message": "Files uploaded successfully",
  "timestamp": "2025-12-09T10:30:00+03:00",
  "request_id": "2d5e2745398c5671",
  "data": {
    "main_file": {
      "id": 15372,
      "name": "test_file.rta",
      "size": 40000,
      "path": "/upload/test_user/RTA/test_file_1764872586_1.rta",
      "crc32": 818415889
    },
    "images": [],
    "summary": {
      "total_files": 1,
      "total_size": 40000,
      "image_count": 0
    }
  }
}

3.2 Загрузка с изображением

HTML Form

<form action="https://api.batterywizard.ru/v2/upload.php" method="post" enctype="multipart/form-data">
  <input type="hidden" name="token" value="bdf380fc8f1234567890abcdef123456">

  <!-- Основной файл -->
  <input type="file" name="upload_file">
  <input type="text" name="filename" value="test_analysis.rta">

  <!-- Изображение -->
  <input type="file" name="upload_photo">
  <input type="number" name="upload_photo_id" value="5">

  <!-- Метаданные -->
  <input type="text" name="version" value="2.1">
  <input type="text" name="uuid" value="device-12345">
  <input type="text" name="os" value="Android 14">
  <input type="text" name="carrier" value="MegaFon">

  <button type="submit">Upload</button>
</form>

Пример ответа с изображением

{
  "success": true,
  "code": 200,
  "message": "Files uploaded successfully",
  "timestamp": "2025-12-09T10:32:15+03:00",
  "request_id": "927760349a58c681",
  "data": {
    "main_file": {
      "id": 15373,
      "name": "test_analysis.rta",
      "size": 30500,
      "path": "/upload/test_user/RTA/test_analysis_1764873135_1.rta",
      "crc32": 2948573621
    },
    "images": [
      {
        "id": 16427,
        "name": "photo1.jpg",
        "size": 2958,
        "content_type_id": 5
      }
    ],
    "summary": {
      "total_files": 2,
      "total_size": 33458,
      "image_count": 1
    }
  }
}

🚀 4. Расширенные возможности

4.1 Множественные изображения с разными ContentType

Структура запроса

POST /api/v2/upload.php HTTP/1.1
Authorization: Bearer bdf380fc8f1234567890abcdef123456
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary123456

------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_file"; filename="analysis.cbe"
Content-Type: application/octet-stream

[бинарные данные файла]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo[]"; filename="overview.jpg"
Content-Type: image/jpeg

[бинарные данные изображения]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo[]"; filename="details.jpg"
Content-Type: image/jpeg

[бинарные данные изображения]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo_id[0]"
Content-Type: text/plain

3
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo_id[1]"
Content-Type: text/plain

7
------WebKitFormBoundary123456
Content-Disposition: form-data; name="filename"
Content-Type: text/plain

full_analysis.cbe

Примечание: Индексы в upload_photo_id[] соответствуют порядку файлов в upload_photo[].

4.2 Base64 кодирование файлов

Для случаев, когда multipart недоступен:

POST /api/v2/upload.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

token=bdf380fc8f1234567890abcdef123456&
file=[BASE64_ENCODED_FILE_CONTENT]&
filename=encoded_file.dat&
version=1.2

4.3 Специфические ContentType ID

Система поддерживает привязку изображений к типам контента через таблицу Content_Type:

IDОписаниеТипичное использование
1Общий видФото батареи целиком
2КлеммыКонтакты и соединения
3МаркировкаСерийные номера, штрих-коды
4ДефектыПовреждения, коррозия
5ГрафикиВизуализации данных
6СхемыЭлектрические схемы
7ДокументыСопутствующая документация

Важно: Если таблица Content_Type не существует или ID не найден, используется значение 0 (без типа).

⚠️ 5. Обработка ответов и ошибок

5.1 Структура успешного ответа

{
  "success": true,
  "code": 200,
  "message": "Files uploaded successfully",
  "timestamp": "2025-12-09T10:30:00+03:00",
  "request_id": "2d5e2745398c5671",
  "data": {
    "main_file": {
      "id": 15372,
      "name": "test_file.rta",
      "size": 40000,
      "path": "/upload/test_user/RTA/test_file_1764872586_1.rta",
      "crc32": 818415889
    },
    "images": [
      {
        "id": 16425,
        "name": "photo.jpg",
        "size": 2958,
        "content_type_id": 5
      }
    ],
    "summary": {
      "total_files": 2,
      "total_size": 42958,
      "image_count": 1
    }
  }
}

5.2 Коды ошибок и их обработка

5.2.1 HTTP 400 — Ошибки клиента

{
  "success": false,
  "code": 400,
  "message": "Exactly one main (non-image) file is required",
  "timestamp": "2025-12-09T10:31:22+03:00",
  "request_id": "33a2726b3f31bf9b"
}

Типичные причины:

  • Не загружены файлы
  • Неправильное количество основных файлов (не 1)
  • Пустые файлы или слишком маленький размер
  • Неподдерживаемое расширение файла

5.2.2 HTTP 401/403 — Ошибки аутентификации

{
  "success": false,
  "code": 403,
  "message": "Invalid or expired authentication token",
  "timestamp": "2025-12-09T10:31:45+03:00",
  "request_id": "49af228aff703066"
}

5.2.3 HTTP 413 — Превышен размер файла

{
  "success": false,
  "code": 413,
  "message": "File 'large_video.mp4' exceeds maximum size of 100.0 MB",
  "timestamp": "2025-12-09T10:32:10+03:00",
  "request_id": "63f072d171d2432e"
}

5.2.4 HTTP 429 — Превышены лимиты

{
  "success": false,
  "code": 429,
  "message": "Daily upload limit exceeded",
  "timestamp": "2025-12-09T10:33:01+03:00",
  "request_id": "400ea9aa4b3ce1a0"
}

5.2.5 HTTP 500 — Внутренние ошибки сервера

{
  "success": false,
  "code": 500,
  "message": "Internal server error",
  "timestamp": "2025-12-09T10:33:30+03:00",
  "request_id": "970dbd275b1f78ae",
  "error_details": "Data too long for column 'SystemPath' at row 1"
}

Примечание: Детали ошибки возвращаются только в тестовом режиме или при включенном debug.

5.3 Test Mode и отладка

Активация тестового режима

POST /api/v2/upload.php
X-Test-Session: test_session_6931d1844125c4.29687211

Расширенный ответ в тестовом режиме

{
  "success": true,
  "code": 200,
  "message": "Files uploaded successfully",
  "timestamp": "2025-12-09T10:34:00+03:00",
  "request_id": "927760349a58c681",
  "data": { ... },
  "debug": {
    "test_mode": true,
    "test_session": "test_session_6931d1844125c4.29687211",
    "debug_log_file": "upload_debug_test_session_6931d1844125c4.29687211_2025-12-04_21-23-06.log",
    "user_id": 391,
    "username": "test_v2",
    "company_id": 3,
    "memory_usage": 2097152,
    "peak_memory": 2097152,
    "debug_entries": 40
  }
}

Логи тестового режима сохраняются в: log/debug/upload_debug_{SESSION_ID}_{TIMESTAMP}.log

📱 6. Особенности работы с данным API из Android приложений

6.1 Рекомендуемые библиотеки

Для HTTP-клиента

// Retrofit + OkHttp (рекомендуется)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

Для работы с Multipart

// Apache Commons IO для упрощения работы с файлами
implementation 'commons-io:commons-io:2.15.1'

6.2 Реализация загрузки на Kotlin

6.2.1 Базовая конфигурация Retrofit

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object ApiClient {
    private const val BASE_URL = "https://api.batterywizard.ru/v2/"
    private const val TIMEOUT = 300L // Увеличено для загрузки файлов

    fun create(): BatteryWizardApi {
        val logging = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        val client = OkHttpClient.Builder()
            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(TIMEOUT, TimeUnit.SECONDS)
            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
            .addInterceptor(logging)
            .addInterceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                    .header("User-Agent", "BatteryWizardAndroid/1.0")
                    .header("Accept", "application/json")
                    .build()
                chain.proceed(request)
            }
            .build()

        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(BatteryWizardApi::class.java)
    }
}

6.2.2 Определение интерфейса API

import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.*

interface BatteryWizardApi {
    @Multipart
    @POST("upload.php")
    fun uploadFile(
        @Header("Authorization") auth: String,
        @Part("filename") filename: RequestBody,
        @Part("version") version: RequestBody,
        @Part("uuid") uuid: RequestBody,
        @Part("os") os: RequestBody,
        @Part("carrier") carrier: RequestBody,
        @Part uploadFile: MultipartBody.Part,
        @Part photos: List<MultipartBody.Part>? = null,
        @Part("upload_photo_id") photoIds: Map<String, RequestBody>? = null
    ): Call<UploadResponse>

    @Multipart
    @POST("upload.php")
    fun uploadFileWithContentType(
        @Header("Authorization") auth: String,
        @Header("X-Test-Session") testSession: String? = null,
        @Part("filename") filename: RequestBody,
        @Part uploadFile: MultipartBody.Part,
        @Part photos: List<MultipartBody.Part>? = null,
        @PartMap contentTypes: Map<String, RequestBody>? = null
    ): Call<UploadResponse>
}

6.2.3 Модели данных

import com.google.gson.annotations.SerializedName

data class UploadResponse(
    @SerializedName("success") val success: Boolean,
    @SerializedName("code") val code: Int,
    @SerializedName("message") val message: String,
    @SerializedName("timestamp") val timestamp: String,
    @SerializedName("request_id") val requestId: String,
    @SerializedName("data") val data: UploadData? = null,
    @SerializedName("debug") val debug: DebugInfo? = null
)

data class UploadData(
    @SerializedName("main_file") val mainFile: MainFile,
    @SerializedName("images") val images: List<ImageInfo>,
    @SerializedName("summary") val summary: UploadSummary
)

data class MainFile(
    @SerializedName("id") val id: Long,
    @SerializedName("name") val name: String,
    @SerializedName("size") val size: Long,
    @SerializedName("path") val path: String,
    @SerializedName("crc32") val crc32: Long
)

data class ImageInfo(
    @SerializedName("id") val id: Long,
    @SerializedName("name") val name: String,
    @SerializedName("size") val size: Long,
    @SerializedName("content_type_id") val contentTypeId: Int
)

data class UploadSummary(
    @SerializedName("total_files") val totalFiles: Int,
    @SerializedName("total_size") val totalSize: Long,
    @SerializedName("image_count") val imageCount: Int
)

data class DebugInfo(
    @SerializedName("test_mode") val testMode: Boolean,
    @SerializedName("test_session") val testSession: String,
    @SerializedName("debug_log_file") val debugLogFile: String,
    @SerializedName("user_id") val userId: Int,
    @SerializedName("username") val username: String,
    @SerializedName("company_id") val companyId: Int,
    @SerializedName("memory_usage") val memoryUsage: Long,
    @SerializedName("peak_memory") val peakMemory: Long,
    @SerializedName("debug_entries") val debugEntries: Int
)

6.2.4 Рабочий пример загрузки

import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File

class FileUploader(private val context: Context) {

    suspend fun uploadBatteryData(
        authToken: String,
        mainFileUri: Uri,
        imageUris: List<Uri>? = null,
        contentTypeIds: List<Int>? = null
    ): Result<UploadResponse> = withContext(Dispatchers.IO) {
        return@withContext try {
            // Подготовка основного файла
            val mainFile = File(mainFileUri.path!!)
            val mainFilePart = MultipartBody.Part.createFormData(
                "upload_file",
                mainFile.name,
                mainFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())
            )

            // Подготовка изображений
            val photoParts = mutableListOf<MultipartBody.Part>()
            val photoIds = mutableMapOf<String, RequestBody>()

            imageUris?.forEachIndexed { index, uri ->
                val imageFile = File(uri.path!!)
                val photoPart = MultipartBody.Part.createFormData(
                    "upload_photo",
                    imageFile.name,
                    imageFile.asRequestBody("image/jpeg".toMediaTypeOrNull())
                )
                photoParts.add(photoPart)

                // Привязка ContentType ID
                contentTypeIds?.getOrNull(index)?.let { contentTypeId ->
                    photoIds["upload_photo_id[$index]"] = 
                        contentTypeId.toString().toRequestBody(MultipartBody.FORM)
                }
            }

            // Метаданные
            val filename = mainFile.name.toRequestBody(MultipartBody.FORM)
            val version = "2.0".toRequestBody(MultipartBody.FORM)
            val uuid = Build.MODEL.toRequestBody(MultipartBody.FORM)
            val os = "Android ${Build.VERSION.RELEASE}".toRequestBody(MultipartBody.FORM)
            val carrier = getCarrierName().toRequestBody(MultipartBody.FORM)

            // Вызов API
            val response = ApiClient.create().uploadFile(
                auth = "Bearer $authToken",
                filename = filename,
                version = version,
                uuid = uuid,
                os = os,
                carrier = carrier,
                uploadFile = mainFilePart,
                photos = if (photoParts.isNotEmpty()) photoParts else null,
                photoIds = if (photoIds.isNotEmpty()) photoIds else null
            ).execute()

            if (response.isSuccessful) {
                Result.success(response.body()!!)
            } else {
                val errorBody = response.errorBody()?.string()
                Result.failure(Exception("Upload failed: ${response.code()} - $errorBody"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    private fun getCarrierName(): String {
        return try {
            val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
            tm.networkOperatorName.takeIf { it.isNotBlank() } ?: "Unknown"
        } catch (e: Exception) {
            "Unknown"
        }
    }
}

6.3 Обработка больших файлов и прогресс

6.3.1 Кастомный Interceptor для отслеживания прогресса

import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class ProgressInterceptor(
    private val progressListener: (bytesWritten: Long, contentLength: Long) -> Unit
) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalResponse = chain.proceed(chain.request())

        return originalResponse.newBuilder()
            .body(originalResponse.body?.let { ProgressResponseBody(it, progressListener) })
            .build()
    }
}

class ProgressResponseBody(
    private val responseBody: ResponseBody,
    private val progressListener: (Long, Long) -> Unit
) : ResponseBody() {

    private var bufferedSource: BufferedSource? = null

    override fun contentType(): MediaType? = responseBody.contentType()

    override fun contentLength(): Long = responseBody.contentLength()

    override fun source(): BufferedSource {
        if (bufferedSource == null) {
            bufferedSource = source(responseBody.source()).buffer()
        }
        return bufferedSource!!
    }

    private fun source(source: Source): Source {
        return object : ForwardingSource(source) {
            var totalBytesRead = 0L

            @Throws(IOException::class)
            override fun read(sink: Buffer, byteCount: Long): Long {
                val bytesRead = super.read(sink, byteCount)
                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
                progressListener(totalBytesRead, responseBody.contentLength())
                return bytesRead
            }
        }
    }
}

6.3.2 Использование в Activity/Fragment

class UploadActivity : AppCompatActivity() {
    private lateinit var binding: ActivityUploadBinding
    private val viewModel: UploadViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityUploadBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupObservers()
    }

    private fun setupObservers() {
        viewModel.uploadProgress.observe(this) { progress ->
            binding.progressBar.progress = progress
            binding.progressText.text = "$progress%"
        }

        viewModel.uploadResult.observe(this) { result ->
            result.onSuccess { response ->
                showSuccess(response)
            }.onFailure { error ->
                showError(error)
            }
        }
    }

    fun startUpload() {
        val fileUri = getFileUriFromIntent()
        viewModel.uploadFile(fileUri)
    }
}

6.4 Особенности Android 10+ (Scoped Storage)

6.4.1 Работа с SAF (Storage Access Framework)

class FilePickerActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_PICK_FILE = 101
        private const val REQUEST_PICK_IMAGES = 102
    }

    fun pickMainFile() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
            putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(
                "application/octet-stream",
                "text/plain",
                "application/json"
            ))
        }
        startActivityForResult(intent, REQUEST_PICK_FILE)
    }

    fun pickImages() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "image/*"
            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        }
        startActivityForResult(intent, REQUEST_PICK_IMAGES)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode == RESULT_OK && data != null) {
            when (requestCode) {
                REQUEST_PICK_FILE -> {
                    val uri = data.data
                    uri?.let { handleFilePicked(it) }
                }
                REQUEST_PICK_IMAGES -> {
                    val uris = mutableListOf<Uri>()
                    if (data.clipData != null) {
                        val clipData = data.clipData!!
                        for (i in 0 until clipData.itemCount) {
                            uris.add(clipData.getItemAt(i).uri)
                        }
                    } else {
                        data.data?.let { uris.add(it) }
                    }
                    handleImagesPicked(uris)
                }
            }
        }
    }

    private fun handleFilePicked(uri: Uri) {
        // Получение временного доступа к файлу
        contentResolver.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION
        )

        // Копирование файла в кэш приложения для загрузки
        copyFileToCache(uri)
    }
}

6.4.2 Копирование файла в кэш

import java.io.File
import java.io.FileOutputStream

private fun copyFileToCache(uri: Uri): File? {
    return try {
        val inputStream = contentResolver.openInputStream(uri)
        val cacheDir = applicationContext.cacheDir
        val tempFile = File.createTempFile("upload_", ".tmp", cacheDir)

        FileOutputStream(tempFile).use { output ->
            inputStream?.copyTo(output)
        }

        inputStream?.close()
        tempFile
    } catch (e: Exception) {
        Log.e("FileUpload", "Error copying file", e)
        null
    }
}

6.5 Оптимизация для мобильных сетей

6.5.1 Автоматическое переключение стратегий

class NetworkAwareUploader(
    private val context: Context,
    private val connectivityManager: ConnectivityManager
) {

    fun uploadWithStrategy(file: File, images: List<File>? = null) {
        val networkInfo = connectivityManager.activeNetworkInfo

        when {
            networkInfo?.type == ConnectivityManager.TYPE_WIFI -> {
                // Полная загрузка с изображениями
                uploadFull(file, images)
            }
            networkInfo?.type == ConnectivityManager.TYPE_MOBILE -> {
                // Только основной файл, изображения отложенно
                uploadMainOnly(file)
                images?.let { scheduleImagesUpload(it) }
            }
            else -> {
                // Оффлайн - планируем загрузку
                scheduleUpload(file, images)
            }
        }
    }

    private fun uploadMainOnly(file: File) {
        // Упрощенная загрузка только основного файла
        // Можно дополнительно сжимать файл перед отправкой
        val compressedFile = compressIfNeeded(file)
        // ... загрузка
    }

    private fun compressIfNeeded(file: File): File {
        // Логика сжатия файлов для мобильных сетей
        return if (file.length() > 5 * 1024 * 1024) { // > 5MB
            compressFile(file)
        } else {
            file
        }
    }
}

⚙️ 7. Тонкая настройка и редкие случаи

7.1 Кастомные заголовки безопасности

7.1.1 Дополнительные заголовки для корпоративных клиентов

POST /api/v2/upload.php HTTP/1.1
Authorization: Bearer bdf380fc8f1234567890abcdef123456
X-Client-Version: 3.2.1
X-Device-ID: android-1234567890abcdef
X-Session-ID: sess_abc123def456
X-Request-Source: mobile_app
X-Compression: gzip
X-Expected-Size: 10485760

7.2 Resumable Upload (частичная загрузка)

Хотя API напрямую не поддерживает resumable upload, можно реализовать на клиенте:

class ResumableUploader {
    private val CHUNK_SIZE = 5 * 1024 * 1024 // 5MB chunks

    suspend fun uploadInChunks(file: File, authToken: String): Result<Unit> {
        val totalChunks = (file.length() / CHUNK_SIZE).toInt() + 1
        var uploadedChunks = loadUploadedChunks(file.name) // Восстановление из SharedPreferences

        for (chunkIndex in uploadedChunks until totalChunks) {
            val chunk = readChunk(file, chunkIndex, CHUNK_SIZE)
            val success = uploadChunk(file.name, chunkIndex, totalChunks, chunk, authToken)

            if (success) {
                saveUploadedChunk(file.name, chunkIndex)
            } else {
                return Result.failure(Exception("Failed at chunk $chunkIndex"))
            }
        }

        return Result.success(Unit)
    }

    private fun readChunk(file: File, chunkIndex: Int, chunkSize: Int): ByteArray {
        RandomAccessFile(file, "r").use { raf ->
            val position = chunkIndex.toLong() * chunkSize
            raf.seek(position)
            val buffer = ByteArray(chunkSize)
            val bytesRead = raf.read(buffer)
            return if (bytesRead == buffer.size) buffer else buffer.copyOf(bytesRead)
        }
    }
}

7.3 Обработка таймаутов и ретраев

class RetryUploader(
    private val maxRetries: Int = 3,
    private val initialDelay: Long = 1000,
    private val maxDelay: Long = 10000
) {

    suspend fun <T> withRetry(
        block: suspend () -> Result<T>
    ): Result<T> {
        var currentDelay = initialDelay
        var lastError: Throwable? = null

        repeat(maxRetries) { attempt ->
            val result = block()
            if (result.isSuccess) {
                return result
            }

            lastError = result.exceptionOrNull()

            // Экспоненциальная backoff с джиттером
            val delayWithJitter = (currentDelay * (0.5 + Math.random())).toLong()
            delay(minOf(delayWithJitter, maxDelay))
            currentDelay *= 2
        }

        return Result.failure(lastError ?: Exception("Max retries exceeded"))
    }
}

7.4 Пакетная загрузка

class BatchUploader {
    private val MAX_CONCURRENT_UPLOADS = 2

    suspend fun uploadBatch(files: List<File>, authToken: String): Map<File, Result<UploadResponse>> {
        return files.chunked(MAX_CONCURRENT_UPLOADS).flatMap { chunk ->
            chunk.map { file ->
                async { file to uploadSingle(file, authToken) }
            }.awaitAll()
        }.toMap()
    }

    private suspend fun uploadSingle(file: File, authToken: String): Result<UploadResponse> {
        // Реализация загрузки одного файла
    }
}

📋 8. Приложения

8.1 Таблица поддерживаемых форматов

Тип файлаРасширенияМаксимальный размерОсобенности
Основные данные.cbe, .rta, .bsl, .dat100 MBТребуют валидации сигнатур
Изображения.jpg, .jpeg, .png, .gif, .webp, .bmp10 MBПроверка MIME-типов
Текстовые.txt, .log, .csv50 MBКодировка UTF-8
Архивы.zip, .rar, .7z200 MBТребуют проверки на вирусы

8.2 Коды ошибок и решения

Код HTTPСообщениеПричинаРешение
400No files uploadedНе переданы файлыПроверить multipart/form-data
400Exactly one main file is requiredНе 1 основной файлПередать ровно 1 не-изображение
400Invalid image file formatПоврежденное изображениеПереконвертировать изображение
403Invalid or expired authentication tokenПросрочен токенОбновить токен через /auth.php
413File exceeds maximum sizeФайл слишком большойРазбить на части или сжать
429Daily upload limit exceededДостигнут лимитОжидать сутки или запросить увеличение
429Too many requestsRate limitingУвеличить интервалы между запросами
500Data too long for column ‘SystemPath’Слишком длинный путьУкоротить имена файлов/папок
500Internal server errorВнутренняя ошибкаСвязаться с поддержкой

8.3 Примеры конфигурационных файлов

8.3.1 AndroidManifest.xml (разрешения)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Интернет -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!-- Для Android 6.0+ -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />

    <!-- Для Android 13+ -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

    <!-- Для работы с камерой (если нужно) -->
    <uses-permission android:name="android.permission.CAMERA" />

    <application>
        <!-- Для Android 7.0+ FileProvider -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

8.3.2 res/xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="cache" path="." />
    <external-cache-path name="external_cache" path="." />
    <external-files-path name="external_files" path="." />
    <files-path name="files" path="." />
</paths>

8.4 Скрипты для тестирования

8.4.1 Bash-скрипт для нагрузочного тестирования

#!/bin/bash
# stress_test_upload.sh

API_URL="https://api.batterywizard.ru/v2/upload.php"
TOKEN="your_token_here"
TEST_FILE="test_file.rta"

# Создание тестового файла
dd if=/dev/urandom of="$TEST_FILE" bs=1M count=10

for i in {1..100}; do
    echo "Upload attempt $i"

    curl -X POST "$API_URL" \
      -H "Authorization: Bearer $TOKEN" \
      -F "upload_file=@$TEST_FILE" \
      -F "filename=test_$i.rta" \
      -F "version=1.0" \
      -F "uuid=test-$i" \
      -F "os=Linux" \
      -F "carrier=Test" \
      --max-time 30 \
      --silent \
      --output /dev/null \
      --write-out "HTTP: %{http_code}, Time: %{time_total}s\n"

    sleep 1
done

rm "$TEST_FILE"

🎯 Заключение

Эндпоинт /api/v2/upload.php предоставляет мощный и гибкий механизм загрузки файлов с поддержкой сложных бизнес-сценариев. При правильной реализации на клиенте (особенно мобильном) обеспечивается:

  1. Надежность — транзакционность, retry-логика, валидация
  2. Безопасность — многоуровневая защита, rate limiting
  3. Гибкость — поддержка различных форматов, контент-типов
  4. Отказоустойчивость — обработка сетевых проблем, таймаутов
  5. Мониторинг — детальное логирование, тестовый режим

Для максимальной эффективности рекомендуется:

  • Использовать рекомендуемые библиотеки и паттерны
  • Реализовать прогресс-индикацию для больших файлов
  • Добавить обработку прерываний и возобновления загрузки
  • Регулярно обновлять токены аутентификации
  • Мониторить лимиты использования API

Техническая поддержка:
При возникновении проблем предоставляйте request_id из ответа API для быстрой диагностики.

Обновления API:
Следите за изменениями в документации BatteryWizard и заголовке X-API-Version в ответах сервера.


Документация актуальна для версии API 2.20.2 (декабрь 2025)
Последнее обновление: 2025-12-09

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх