feat: create upload functions

This commit is contained in:
Andrey Ponomarenko 2020-09-24 14:18:42 +03:00
parent e01b22eda1
commit ac34be1528
13 changed files with 2420 additions and 57 deletions

View File

@ -29,8 +29,9 @@
"settings": {},
"rules": {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types" : 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/camelcase": 0,
"camelcase": 0,
"@typescript-eslint/no-empty-function": 0,
"no-unused-expressions": 0,
"indent": [

View File

@ -4,16 +4,21 @@
"main": "index.js",
"author": "Andrey Ponomarenko",
"license": "MIT",
"scripts": {
"r": "yarn webpack && node dist/index.js"
},
"dependencies": {
"form-data": "^3.0.0",
"node-fetch": "^2.6.1",
"promise.allsettled": "^1.0.2",
"ts-node": "^8.10.2",
"typescript": "^3.9.7"
},
"devDependencies": {
"husky": "^4.3.0",
"@commitlint/cli": "^9.1.2",
"@commitlint/config-conventional": "^9.1.2",
"@types/fs-extra": "^9.0.1",
"@types/node-fetch": "^2.5.7",
"@types/promise.allsettled": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^3.7.1",
"@typescript-eslint/parser": "^3.7.1",
@ -22,7 +27,11 @@
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.4",
"ts-loader": "^8.0.2"
"husky": "^4.3.0",
"ts-loader": "^8.0.2",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-node-externals": "^2.5.0"
},
"husky": {
"hooks": {

9
src/constants/index.ts Normal file
View File

@ -0,0 +1,9 @@
export const API_UPLOAD = 'https://upload.diawi.com';
export const API_STATUS = 'https://upload.diawi.com/status';
export const MAX_API_STATUS_CALLS = 30;
export enum JOB_STATUS {
OK = 2000,
PROCEEDING = 2001,
ERROR = 4000,
}

View File

@ -0,0 +1,10 @@
import { API_STATUS } from '@app/constants';
import { ApiStatusParams, ApiStatusResponse } from '@app/types';
import fetch from 'node-fetch';
export const fetchJobStatus = async ({ token, job }: ApiStatusParams): Promise<ApiStatusResponse> => {
const url = `${API_STATUS}/?token=${token}&job=${job}`;
const res = await fetch(url);
return res.json();
};

4
src/core/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { fetchJobStatus } from './fetchJobStatus';
export { rawUpload } from './rawUpload';
export { request } from './request';
export { upload } from './upload';

View File

@ -0,0 +1,41 @@
import { AnyFunction, ApiUploadProps, ApiUploadResponse, UploadOptions } from '@app/types';
import * as fs from 'fs';
import * as path from 'path';
import { createFormData, noop } from '@app/utils';
import { API_UPLOAD } from '@app/constants';
import { request } from '@app/core';
// upload file, without checking the status
export const rawUpload = async (params: UploadOptions): Promise<ApiUploadResponse> => {
const {
file,
onUploadProgress = noop,
...restParams
} = params;
const filePath = path.resolve(file);
// check file existence
if (!fs.existsSync(filePath)) throw new Error(`file ${filePath} not found`);
const fileStream = fs.createReadStream(filePath);
const form = createFormData({ file: fileStream, ...restParams });
// displays file upload progress
const fileSize = fs.lstatSync(filePath).size;
let bytesWritten = 0;
fileStream.on('data', (chunk) => {
bytesWritten += chunk.length;
const progressPercent = ((bytesWritten / fileSize) * 100).toFixed(2);
onUploadProgress({ progressPercent, bytesWritten, fileSize });
});
const data = await request({
method: 'post',
host: new URL(API_UPLOAD).host,
headers: form.getHeaders(),
formData: form,
});
return JSON.parse(data);
};

29
src/core/request/index.ts Normal file
View File

@ -0,0 +1,29 @@
import * as https from 'https';
import { RequestOptions } from 'https';
import * as FormData from 'form-data';
interface Params extends RequestOptions {
formData: FormData
}
export const request = async (props: Params): Promise<any> => {
const { formData, ...requestOptions } = props;
return new Promise((resolve, reject) => {
const request = https.request(requestOptions, (request) => {
request.on('data', (data) => {
resolve(data);
});
request.on('error', reject);
});
request.on('response', (res) => {
if (res.statusCode !== 200) {
reject(new Error(`API Response error: Status: ${res.statusCode}; Message: ${res.statusMessage} `));
}
});
formData.pipe(request);
});
};

46
src/core/upload/index.ts Normal file
View File

@ -0,0 +1,46 @@
import { ApiStatusResponse, UploadOptions } from '@app/types';
import { rawUpload, fetchJobStatus } from '@app/core';
import { JOB_STATUS, MAX_API_STATUS_CALLS } from '@app/constants';
interface Options {
maxApiStatusCalls?: number;
}
// upload file, and wait for proceeding
export const upload = async (props: UploadOptions, options: Options = {}) => {
const { token } = props;
const {
maxApiStatusCalls = MAX_API_STATUS_CALLS,
} = options;
const { job } = await rawUpload(props);
let statusCallsCount = 0;
const checkStatus = async (): Promise<ApiStatusResponse> => {
const jobStatus = await fetchJobStatus({ token, job });
const { status, message } = jobStatus;
if (statusCallsCount > maxApiStatusCalls) {
statusCallsCount += 1;
throw new Error('max status api calls exceeded');
}
console.log('status', status);
switch (status) {
case JOB_STATUS.ERROR: {
throw new Error(message);
}
case JOB_STATUS.PROCEEDING: {
console.log('status PROCEEDING', status);
return checkStatus();
break;
}
default: {
return jobStatus;
}
}
};
return checkStatus();
};

View File

@ -1,9 +1,8 @@
export interface A {
a: 1,
}
// import { upload } from '@app/core';
export const z = ():A => ({
a: 1,
});
(async () => {
// const res = await upload({ token, file });
console.log(123);
console.dir('============== COMPLETE ==============');
// console.dir(res);
})();

39
src/types/index.ts Normal file
View File

@ -0,0 +1,39 @@
/* eslint-disable no-multi-spaces */
export interface ApiUploadProps {
token: string; // your API access token
file: string; // path to file
find_by_udid?: boolean; // allow your testers to find the app on Diawi's mobile web app using their UDID (iOS only)
wall_of_apps?: boolean; // allow Diawi to display the app's icon on the wall of apps
password?: string; // protect your app with a password; it will be required to access the installation page
comment?: string; // additional information to your users on this build; the comment will be displayed on the installation page
callback_url?: string; // the URL Diawi should call with the result
callback_emails?: string; // the email addresses Diawi will send the result to (up to 5 separated by commas for starter/premium/enterprise accounts, 1 for free accounts)
installation_notifications?: boolean; // receive notifications each time someone installs the app (only starter/premium/enterprise accounts)
}
export interface UploadOptions extends ApiUploadProps {
onUploadProgress?: AnyFunction;
}
export interface ApiUploadResponse {
job: string;
}
export interface ApiStatusParams {
token: string; // your API access token
job: string; // the upload job hash from the upload response
}
export interface ApiStatusResponse {
status: number;
message: string;
hash?: string;
link?: string;
qrcode?: string,
links?: string[],
qrcodes?: string[]
}
export type AnyFunction = (...args: any) => any;
export type AnyObject = Record<any, any>;

14
src/utils/index.ts Normal file
View File

@ -0,0 +1,14 @@
import * as FormData from 'form-data';
// async await timer
export const sleep = async (ms: number): Promise<void> => (
new Promise((resolve) => setTimeout(resolve, ms))
);
export const createFormData = (data: Record<string, any>): FormData => {
const formData = new FormData();
Object.keys(data).forEach((key) => formData.append(key, data[key]));
return formData;
};
export const noop = () => {};

34
webpack.config.js Normal file
View File

@ -0,0 +1,34 @@
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const webpack = require('webpack');
module.exports = {
entry: {
core: './src/core/index.ts',
},
output: {
filename: '[name]/index.js',
path: `${__dirname}/dist`,
},
mode: 'development',
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
target: 'node',
resolve: {
alias: {
'@app': path.resolve(__dirname, './src'),
},
extensions: ['.ts', '.js'],
},
plugins: [
new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true }),
],
};

2222
yarn.lock

File diff suppressed because it is too large Load Diff