@k03mad/ip2geo 19.3.3 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +2 -1
- package/app/api.ts +120 -0
- package/app/cli.ts +102 -0
- package/app/helpers/cache.ts +257 -0
- package/app/helpers/utils.ts +1 -0
- package/app/types.ts +15 -0
- package/cli.js +3 -0
- package/package.json +14 -11
- package/pnpm-workspace.yaml +4 -42
- package/tests/cache-file-entries.ts +37 -0
- package/tests/cache-file-modify.ts +34 -0
- package/tests/cache-file-subfolder.ts +34 -0
- package/tests/cache-map-entries.ts +41 -0
- package/tests/cache-map-max-entries.ts +50 -0
- package/tests/cache-map-off.ts +34 -0
- package/tests/default.ts +55 -0
- package/tests/helpers/path.ts +5 -0
- package/tests/ip-multi-requests-cache.ts +39 -0
- package/tests/ip-no-data.ts +33 -0
- package/tests/ip-v6.ts +31 -0
- package/tests/shared/consts.ts +49 -0
- package/tests/shared/fs.ts +44 -0
- package/tsconfig.json +9 -0
- package/app/api.js +0 -142
- package/app/cli.js +0 -104
- package/app/helpers/cache.js +0 -274
- package/app/helpers/utils.js +0 -2
- package/tests/cache-file-entries.js +0 -37
- package/tests/cache-file-modify.js +0 -34
- package/tests/cache-file-subfolder.js +0 -34
- package/tests/cache-map-entries.js +0 -41
- package/tests/cache-map-max-entries.js +0 -50
- package/tests/cache-map-off.js +0 -35
- package/tests/default.js +0 -55
- package/tests/helpers/path.js +0 -13
- package/tests/ip-multi-requests-cache.js +0 -39
- package/tests/ip-no-data.js +0 -35
- package/tests/ip-v6.js +0 -31
- package/tests/shared/consts.js +0 -47
- package/tests/shared/fs.js +0 -42
- /package/app/helpers/{colors.js → colors.ts} +0 -0
package/.vscode/settings.json
CHANGED
package/app/api.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
collectOutputData,
|
|
6
|
+
readFromFsCache,
|
|
7
|
+
readFromMapCache,
|
|
8
|
+
writeToFsCache,
|
|
9
|
+
writeToMapCache,
|
|
10
|
+
} from './helpers/cache.ts';
|
|
11
|
+
import type {ReqOutput} from './types.ts';
|
|
12
|
+
|
|
13
|
+
const API = 'https://ipwho.is/';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_CACHE_FILE_DIR = path.join(os.tmpdir(), '.ip2geo-cache');
|
|
16
|
+
export const DEFAULT_CACHE_FILE_NAME = 'ip.log';
|
|
17
|
+
export const DEFAULT_CACHE_FILE_SEPARATOR = ';;';
|
|
18
|
+
export const DEFAULT_CACHE_FILE_NEWLINE = '\n';
|
|
19
|
+
export const DEFAULT_CACHE_MAP_MAX_ENTRIES = Number.POSITIVE_INFINITY;
|
|
20
|
+
|
|
21
|
+
export interface ReqInput {
|
|
22
|
+
ip?: string;
|
|
23
|
+
cacheDir?: string;
|
|
24
|
+
cacheFileName?: string;
|
|
25
|
+
cacheFileSeparator?: string;
|
|
26
|
+
cacheFileNewline?: string;
|
|
27
|
+
cacheMap?: Map<string, ReqOutput>;
|
|
28
|
+
cacheMapMaxEntries?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ApiResponseBody {
|
|
32
|
+
ip?: string;
|
|
33
|
+
continent?: string;
|
|
34
|
+
continent_code?: string;
|
|
35
|
+
country?: string;
|
|
36
|
+
country_code?: string;
|
|
37
|
+
flag?: {emoji?: string};
|
|
38
|
+
region?: string;
|
|
39
|
+
region_code?: string;
|
|
40
|
+
city?: string;
|
|
41
|
+
connection?: {
|
|
42
|
+
asn?: number | string;
|
|
43
|
+
org?: string;
|
|
44
|
+
isp?: string;
|
|
45
|
+
domain?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const cacheStorage = new Map<string, ReqOutput>();
|
|
50
|
+
|
|
51
|
+
export const ip2geo = async ({
|
|
52
|
+
ip = '',
|
|
53
|
+
cacheDir = DEFAULT_CACHE_FILE_DIR,
|
|
54
|
+
cacheFileName = DEFAULT_CACHE_FILE_NAME,
|
|
55
|
+
cacheFileSeparator = DEFAULT_CACHE_FILE_SEPARATOR,
|
|
56
|
+
cacheFileNewline = DEFAULT_CACHE_FILE_NEWLINE,
|
|
57
|
+
cacheMapMaxEntries = DEFAULT_CACHE_MAP_MAX_ENTRIES,
|
|
58
|
+
cacheMap = cacheStorage,
|
|
59
|
+
}: ReqInput = {}): Promise<ReqOutput> => {
|
|
60
|
+
if (ip) {
|
|
61
|
+
const mapCache = readFromMapCache(ip, cacheMap, cacheMapMaxEntries);
|
|
62
|
+
|
|
63
|
+
if (mapCache) {
|
|
64
|
+
return mapCache;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fsCache = await readFromFsCache(
|
|
68
|
+
ip,
|
|
69
|
+
cacheDir,
|
|
70
|
+
cacheFileName,
|
|
71
|
+
cacheFileSeparator,
|
|
72
|
+
cacheFileNewline,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (fsCache) {
|
|
76
|
+
writeToMapCache(fsCache, cacheMap, cacheMapMaxEntries);
|
|
77
|
+
|
|
78
|
+
return fsCache;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reqUrl = API + ip;
|
|
83
|
+
const response = await fetch(reqUrl);
|
|
84
|
+
const body = (await response.json()) as ApiResponseBody;
|
|
85
|
+
|
|
86
|
+
if (!body?.ip) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
['API error', `request: ${reqUrl}`, `response body: ${JSON.stringify(body)}`].join('\n'),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const outputData = collectOutputData([
|
|
93
|
+
body.ip ?? '',
|
|
94
|
+
body?.continent ?? '',
|
|
95
|
+
body?.continent_code ?? '',
|
|
96
|
+
body?.country ?? '',
|
|
97
|
+
body?.country_code ?? '',
|
|
98
|
+
body?.flag?.emoji ?? '',
|
|
99
|
+
body?.region ?? '',
|
|
100
|
+
body?.region_code ?? '',
|
|
101
|
+
body?.city ?? '',
|
|
102
|
+
String(body?.connection?.asn ?? ''),
|
|
103
|
+
body?.connection?.org ?? '',
|
|
104
|
+
body?.connection?.isp ?? '',
|
|
105
|
+
body?.connection?.domain ?? '',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
writeToMapCache(outputData, cacheMap, cacheMapMaxEntries);
|
|
109
|
+
|
|
110
|
+
await writeToFsCache(
|
|
111
|
+
body.ip ?? '',
|
|
112
|
+
outputData,
|
|
113
|
+
cacheDir,
|
|
114
|
+
cacheFileName,
|
|
115
|
+
cacheFileSeparator,
|
|
116
|
+
cacheFileNewline,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return outputData;
|
|
120
|
+
};
|
package/app/cli.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_CACHE_FILE_DIR,
|
|
5
|
+
DEFAULT_CACHE_FILE_NEWLINE,
|
|
6
|
+
DEFAULT_CACHE_FILE_SEPARATOR,
|
|
7
|
+
ip2geo,
|
|
8
|
+
} from './api.ts';
|
|
9
|
+
import {pruneCache} from './helpers/cache.ts';
|
|
10
|
+
import {codeText, nameText} from './helpers/colors.ts';
|
|
11
|
+
|
|
12
|
+
const {blue, red, green, dim, bold} = chalk;
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const argsExtra = args.filter(arg => !arg.startsWith('-'));
|
|
16
|
+
|
|
17
|
+
const isHelp = args.includes('-h') || args.includes('--help');
|
|
18
|
+
const isPrune = args.includes('-p') || args.includes('--prune');
|
|
19
|
+
|
|
20
|
+
if (isHelp) {
|
|
21
|
+
const cmd = `${codeText('$')} ${nameText('ip2geo')}`;
|
|
22
|
+
|
|
23
|
+
console.log(
|
|
24
|
+
[
|
|
25
|
+
'',
|
|
26
|
+
codeText('# current external ip'),
|
|
27
|
+
cmd,
|
|
28
|
+
`${cmd} 1.1.1.1`,
|
|
29
|
+
'',
|
|
30
|
+
`${cmd} -h`,
|
|
31
|
+
`${cmd} --help`,
|
|
32
|
+
'',
|
|
33
|
+
`${cmd} 1.1.1.1 -j`,
|
|
34
|
+
`${cmd} 1.1.1.1 --json`,
|
|
35
|
+
'',
|
|
36
|
+
`${cmd} 1.1.1.1 8.8.8.8`,
|
|
37
|
+
`${cmd} 1.1.1.1,8.8.8.8`,
|
|
38
|
+
'',
|
|
39
|
+
codeText('# remove duplicate cache entries'),
|
|
40
|
+
`${cmd} -p`,
|
|
41
|
+
`${cmd} --prune`,
|
|
42
|
+
].join('\n'),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isPrune) {
|
|
49
|
+
const cacheFolder = argsExtra[0] ?? DEFAULT_CACHE_FILE_DIR;
|
|
50
|
+
console.log(blue(cacheFolder));
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const {different, duplicates, empty, entries, longLinesFiles} = await pruneCache(
|
|
54
|
+
cacheFolder,
|
|
55
|
+
DEFAULT_CACHE_FILE_SEPARATOR,
|
|
56
|
+
DEFAULT_CACHE_FILE_NEWLINE,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
[
|
|
61
|
+
'',
|
|
62
|
+
green(`Removed duplicate cache entries: ${bold(duplicates.length)}`),
|
|
63
|
+
...duplicates.map(elem => dim(`— ${elem}`)),
|
|
64
|
+
green(`Removed different cache entries: ${bold(different.length)}`),
|
|
65
|
+
...different.map(elem => dim(`— ${elem}`)),
|
|
66
|
+
green(`Removed empty cache entries: ${bold(empty.length)}`),
|
|
67
|
+
...empty.map(elem => dim(`— ${elem}`)),
|
|
68
|
+
].join('\n'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (longLinesFiles.length > 0) {
|
|
72
|
+
console.log(
|
|
73
|
+
[
|
|
74
|
+
red(
|
|
75
|
+
`Required manual check, some cache files has too long lines: ${bold(longLinesFiles.length)}`,
|
|
76
|
+
),
|
|
77
|
+
...longLinesFiles.map(({file, elem}) => dim(`— ${file}\n|— ${elem}`)),
|
|
78
|
+
].join('\n'),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(['', green(`Current cache entries: ${bold(entries)}`)].join('\n'));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(red(String(err)));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const output =
|
|
89
|
+
argsExtra.length === 0
|
|
90
|
+
? [await ip2geo()]
|
|
91
|
+
: await Promise.all(
|
|
92
|
+
argsExtra.map(arg => Promise.all(arg.split(',').map(ip => ip2geo({ip})))),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const flatten = output.flat();
|
|
96
|
+
|
|
97
|
+
if (args.includes('--json') || args.includes('-j')) {
|
|
98
|
+
console.log(JSON.stringify(flatten.length > 1 ? flatten : flatten[0]));
|
|
99
|
+
} else {
|
|
100
|
+
flatten.forEach(elem => console.log(elem));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import _debug from 'debug';
|
|
5
|
+
import {isIP} from 'is-ip';
|
|
6
|
+
|
|
7
|
+
import type {ReqOutput} from '../types.ts';
|
|
8
|
+
|
|
9
|
+
import {getArrayDups} from './utils.ts';
|
|
10
|
+
|
|
11
|
+
const debug = _debug('mad:geoip');
|
|
12
|
+
|
|
13
|
+
type CacheMap = Map<string, ReqOutput>;
|
|
14
|
+
|
|
15
|
+
interface LongLinesFile {
|
|
16
|
+
file: string;
|
|
17
|
+
elem: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PruneCacheResult {
|
|
21
|
+
entries: number;
|
|
22
|
+
duplicates: string[];
|
|
23
|
+
different: string[];
|
|
24
|
+
empty: string[];
|
|
25
|
+
longLinesFiles: LongLinesFile[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const outputKeys = [
|
|
29
|
+
'ip',
|
|
30
|
+
'continent',
|
|
31
|
+
'continentCode',
|
|
32
|
+
'country',
|
|
33
|
+
'countryCode',
|
|
34
|
+
'countryEmoji',
|
|
35
|
+
'region',
|
|
36
|
+
'regionCode',
|
|
37
|
+
'city',
|
|
38
|
+
'connectionAsn',
|
|
39
|
+
'connectionOrg',
|
|
40
|
+
'connectionIsp',
|
|
41
|
+
'connectionDomain',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export const collectOutputData = (dataArr: string[]): ReqOutput => {
|
|
45
|
+
const outputData = {} as Record<(typeof outputKeys)[number], string | number | undefined>;
|
|
46
|
+
|
|
47
|
+
outputKeys.forEach((key, i) => {
|
|
48
|
+
outputData[key] = key === 'connectionAsn' && dataArr[i] ? Number(dataArr[i]) : dataArr[i];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return outputData as ReqOutput;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getCacheFileFullPath = (ip: string, cacheDir: string, cacheFileName: string): string => {
|
|
55
|
+
const [firstOctet] = ip.split(/[.:]/);
|
|
56
|
+
return path.join(cacheDir, `${firstOctet}_${cacheFileName}`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const readFromFsCache = async (
|
|
60
|
+
ip: string,
|
|
61
|
+
cacheDir: string,
|
|
62
|
+
cacheFileName: string,
|
|
63
|
+
cacheFileSeparator: string,
|
|
64
|
+
cacheFileNewline: string,
|
|
65
|
+
): Promise<ReqOutput | undefined> => {
|
|
66
|
+
const cacheFileFull = getCacheFileFullPath(ip, cacheDir, cacheFileName);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await fs.mkdir(cacheDir, {recursive: true});
|
|
70
|
+
const content = await fs.readFile(cacheFileFull, {encoding: 'utf8'});
|
|
71
|
+
|
|
72
|
+
if (content) {
|
|
73
|
+
const data = content.split(cacheFileNewline);
|
|
74
|
+
|
|
75
|
+
for (const elem of data) {
|
|
76
|
+
const fileData = elem.split(cacheFileSeparator);
|
|
77
|
+
|
|
78
|
+
if (ip === fileData[0]) {
|
|
79
|
+
const outputData = collectOutputData(fileData);
|
|
80
|
+
debug('get from fs cache: %o %o', cacheFileFull, outputData);
|
|
81
|
+
return outputData;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
87
|
+
await fs.appendFile(cacheFileFull, '');
|
|
88
|
+
} else {
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const writeToFsCache = async (
|
|
97
|
+
ip: string,
|
|
98
|
+
data: ReqOutput,
|
|
99
|
+
cacheDir: string,
|
|
100
|
+
cacheFileName: string,
|
|
101
|
+
cacheFileSeparator: string,
|
|
102
|
+
cacheFileNewline: string,
|
|
103
|
+
): Promise<void> => {
|
|
104
|
+
const cacheFileFull = getCacheFileFullPath(ip, cacheDir, cacheFileName);
|
|
105
|
+
debug('set to fs cache: %o %o', cacheFileFull, data);
|
|
106
|
+
|
|
107
|
+
await fs.mkdir(cacheDir, {recursive: true});
|
|
108
|
+
|
|
109
|
+
await fs.appendFile(
|
|
110
|
+
cacheFileFull,
|
|
111
|
+
cacheFileNewline + Object.values(data).join(cacheFileSeparator),
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const readFromMapCache = (
|
|
116
|
+
ip: string,
|
|
117
|
+
cacheMap: CacheMap,
|
|
118
|
+
cacheMapMaxEntries: number,
|
|
119
|
+
): ReqOutput | undefined => {
|
|
120
|
+
if (cacheMapMaxEntries > 0) {
|
|
121
|
+
const value = cacheMap.get(ip);
|
|
122
|
+
|
|
123
|
+
if (value) {
|
|
124
|
+
debug('get from map cache: %o', value);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const removeFromMapCacheIfLimit = (cacheMap: CacheMap, cacheMapMaxEntries: number): void => {
|
|
133
|
+
if (cacheMap.size > cacheMapMaxEntries) {
|
|
134
|
+
debug('remove from map cache by limit: %o > %o', cacheMap.size, cacheMapMaxEntries);
|
|
135
|
+
|
|
136
|
+
for (const [key] of cacheMap) {
|
|
137
|
+
debug('remove from map cache by limit: %o', key);
|
|
138
|
+
cacheMap.delete(key);
|
|
139
|
+
|
|
140
|
+
if (cacheMap.size <= cacheMapMaxEntries) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const writeToMapCache = (
|
|
148
|
+
body: ReqOutput,
|
|
149
|
+
cacheMap: CacheMap,
|
|
150
|
+
cacheMapMaxEntries: number,
|
|
151
|
+
): void => {
|
|
152
|
+
if (cacheMapMaxEntries > 0) {
|
|
153
|
+
debug('set to map cache: %o', body);
|
|
154
|
+
cacheMap.set(body.ip ?? '', body);
|
|
155
|
+
|
|
156
|
+
removeFromMapCacheIfLimit(cacheMap, cacheMapMaxEntries);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const pruneCache = async (
|
|
161
|
+
cacheDir: string,
|
|
162
|
+
cacheFileSeparator: string,
|
|
163
|
+
cacheFileNewline: string,
|
|
164
|
+
): Promise<PruneCacheResult> => {
|
|
165
|
+
const files = await fs.readdir(cacheDir);
|
|
166
|
+
|
|
167
|
+
await Promise.all(
|
|
168
|
+
files.map(async file => {
|
|
169
|
+
const fullFilePath = path.join(cacheDir, file);
|
|
170
|
+
|
|
171
|
+
const [stat, data] = await Promise.all([
|
|
172
|
+
fs.lstat(fullFilePath),
|
|
173
|
+
fs.readFile(fullFilePath, {encoding: 'utf8'}),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const firstIp = data?.split(cacheFileNewline)?.find(Boolean)?.split(cacheFileSeparator)[0];
|
|
177
|
+
|
|
178
|
+
if (stat.isDirectory() || (firstIp && !isIP(firstIp))) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Folder has subfolders or files without IPs, wrong cache folder arg?\n${fullFilePath}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const cacheLineToNum = (line: string): number =>
|
|
187
|
+
Number(
|
|
188
|
+
(line.split(cacheFileSeparator)[0] ?? '')
|
|
189
|
+
.split('.')
|
|
190
|
+
.map(num => `00${num}`.slice(-3))
|
|
191
|
+
.join(''),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const duplicates = new Set<string>();
|
|
195
|
+
const different = new Set<string>();
|
|
196
|
+
const empty: string[] = [];
|
|
197
|
+
const longLinesFiles = new Set<LongLinesFile>();
|
|
198
|
+
let entries = 0;
|
|
199
|
+
|
|
200
|
+
await Promise.all(
|
|
201
|
+
files.map(async file => {
|
|
202
|
+
const fullFilePath = path.join(cacheDir, file);
|
|
203
|
+
|
|
204
|
+
const data = await fs.readFile(fullFilePath, {encoding: 'utf8'});
|
|
205
|
+
const dataArr = data.split(cacheFileNewline).filter(Boolean);
|
|
206
|
+
|
|
207
|
+
const dataArrRemoveEmpty = dataArr.filter((elem): boolean => {
|
|
208
|
+
const splitted = elem.split(cacheFileSeparator);
|
|
209
|
+
|
|
210
|
+
if (splitted.length > outputKeys.length) {
|
|
211
|
+
longLinesFiles.add({file: fullFilePath, elem});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (splitted.filter(Boolean).length > 1) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
empty.push(elem);
|
|
219
|
+
return false;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const uniqSorted = [...new Set(dataArrRemoveEmpty)].toSorted(
|
|
223
|
+
(a, b) => cacheLineToNum(a) - cacheLineToNum(b),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
getArrayDups(dataArrRemoveEmpty).forEach(dup => duplicates.add(dup));
|
|
227
|
+
|
|
228
|
+
const dupsIp = getArrayDups(uniqSorted.map(elem => elem.split(cacheFileSeparator)[0]));
|
|
229
|
+
|
|
230
|
+
const removeDiffs = uniqSorted.filter(elem => {
|
|
231
|
+
if (dupsIp.includes(elem.split(cacheFileSeparator)[0])) {
|
|
232
|
+
different.add(elem);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const fileContent = removeDiffs.join(cacheFileNewline).trim();
|
|
240
|
+
|
|
241
|
+
if (fileContent) {
|
|
242
|
+
await fs.writeFile(fullFilePath, fileContent);
|
|
243
|
+
entries += removeDiffs.length;
|
|
244
|
+
} else {
|
|
245
|
+
await fs.rm(fullFilePath);
|
|
246
|
+
}
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
entries,
|
|
252
|
+
duplicates: [...duplicates],
|
|
253
|
+
different: [...different],
|
|
254
|
+
empty,
|
|
255
|
+
longLinesFiles: [...longLinesFiles],
|
|
256
|
+
};
|
|
257
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const getArrayDups = <T>(arr: T[]): T[] => arr.filter((e, i, a) => a.indexOf(e) !== i);
|
package/app/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ReqOutput {
|
|
2
|
+
ip?: string;
|
|
3
|
+
continent?: string;
|
|
4
|
+
continentCode?: string;
|
|
5
|
+
country?: string;
|
|
6
|
+
countryCode?: string;
|
|
7
|
+
countryEmoji?: string;
|
|
8
|
+
region?: string;
|
|
9
|
+
regionCode?: string;
|
|
10
|
+
city?: string;
|
|
11
|
+
connectionAsn?: number;
|
|
12
|
+
connectionOrg?: string;
|
|
13
|
+
connectionIsp?: string;
|
|
14
|
+
connectionDomain?: string;
|
|
15
|
+
}
|
package/cli.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k03mad/ip2geo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.0.0",
|
|
4
4
|
"description": "GeoIP library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"maintainers": [
|
|
@@ -11,27 +11,30 @@
|
|
|
11
11
|
"url": "git+https://github.com/k03mad/ip2geo.git"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"ip2geo": "
|
|
14
|
+
"ip2geo": "cli.js"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
-
"main": "app/api.
|
|
17
|
+
"main": "app/api.ts",
|
|
18
18
|
"scripts": {
|
|
19
|
-
"lint": "oxlint && oxfmt -c node_modules/@k03mad/oxlint-config/.oxfmtrc.json --check",
|
|
20
|
-
"test": "rm -rfv ./.geoip &&
|
|
19
|
+
"lint": "oxlint && oxfmt -c node_modules/@k03mad/oxlint-config/.oxfmtrc.json --check && tsc --noEmit",
|
|
20
|
+
"test": "rm -rfv ./.geoip && node --test tests/*.ts",
|
|
21
21
|
"prepare": "husky || true"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@k03mad/request": "15.3.3",
|
|
25
24
|
"chalk": "5.6.2",
|
|
26
25
|
"debug": "4.4.3",
|
|
27
|
-
"is-ip": "5.0.1"
|
|
26
|
+
"is-ip": "5.0.1",
|
|
27
|
+
"tsx": "4.23.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@k03mad/oxlint-config": "0.
|
|
30
|
+
"@k03mad/oxlint-config": "0.11.0",
|
|
31
|
+
"@tsconfig/strictest": "2.0.8",
|
|
32
|
+
"@types/debug": "4.1.12",
|
|
33
|
+
"@types/node": "26.1.0",
|
|
31
34
|
"husky": "9.1.7",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
+
"oxfmt": "0.57.0",
|
|
36
|
+
"oxlint": "1.72.0",
|
|
37
|
+
"typescript": "6.0.3"
|
|
35
38
|
},
|
|
36
39
|
"engines": {
|
|
37
40
|
"node": ">=24"
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -1,43 +1,5 @@
|
|
|
1
|
+
allowBuilds:
|
|
2
|
+
esbuild: false
|
|
1
3
|
minimumReleaseAgeExclude:
|
|
2
|
-
- '@k03mad/oxlint-config@0.
|
|
3
|
-
-
|
|
4
|
-
- '@oxfmt/binding-android-arm-eabi@0.53.0'
|
|
5
|
-
- '@oxfmt/binding-android-arm64@0.53.0'
|
|
6
|
-
- '@oxfmt/binding-darwin-arm64@0.53.0'
|
|
7
|
-
- '@oxfmt/binding-darwin-x64@0.53.0'
|
|
8
|
-
- '@oxfmt/binding-freebsd-x64@0.53.0'
|
|
9
|
-
- '@oxfmt/binding-linux-arm-gnueabihf@0.53.0'
|
|
10
|
-
- '@oxfmt/binding-linux-arm-musleabihf@0.53.0'
|
|
11
|
-
- '@oxfmt/binding-linux-arm64-gnu@0.53.0'
|
|
12
|
-
- '@oxfmt/binding-linux-arm64-musl@0.53.0'
|
|
13
|
-
- '@oxfmt/binding-linux-ppc64-gnu@0.53.0'
|
|
14
|
-
- '@oxfmt/binding-linux-riscv64-gnu@0.53.0'
|
|
15
|
-
- '@oxfmt/binding-linux-riscv64-musl@0.53.0'
|
|
16
|
-
- '@oxfmt/binding-linux-s390x-gnu@0.53.0'
|
|
17
|
-
- '@oxfmt/binding-linux-x64-gnu@0.53.0'
|
|
18
|
-
- '@oxfmt/binding-linux-x64-musl@0.53.0'
|
|
19
|
-
- '@oxfmt/binding-openharmony-arm64@0.53.0'
|
|
20
|
-
- '@oxfmt/binding-win32-arm64-msvc@0.53.0'
|
|
21
|
-
- '@oxfmt/binding-win32-ia32-msvc@0.53.0'
|
|
22
|
-
- '@oxfmt/binding-win32-x64-msvc@0.53.0'
|
|
23
|
-
- '@oxlint/binding-android-arm-eabi@1.68.0'
|
|
24
|
-
- '@oxlint/binding-android-arm64@1.68.0'
|
|
25
|
-
- '@oxlint/binding-darwin-arm64@1.68.0'
|
|
26
|
-
- '@oxlint/binding-darwin-x64@1.68.0'
|
|
27
|
-
- '@oxlint/binding-freebsd-x64@1.68.0'
|
|
28
|
-
- '@oxlint/binding-linux-arm-gnueabihf@1.68.0'
|
|
29
|
-
- '@oxlint/binding-linux-arm-musleabihf@1.68.0'
|
|
30
|
-
- '@oxlint/binding-linux-arm64-gnu@1.68.0'
|
|
31
|
-
- '@oxlint/binding-linux-arm64-musl@1.68.0'
|
|
32
|
-
- '@oxlint/binding-linux-ppc64-gnu@1.68.0'
|
|
33
|
-
- '@oxlint/binding-linux-riscv64-gnu@1.68.0'
|
|
34
|
-
- '@oxlint/binding-linux-riscv64-musl@1.68.0'
|
|
35
|
-
- '@oxlint/binding-linux-s390x-gnu@1.68.0'
|
|
36
|
-
- '@oxlint/binding-linux-x64-gnu@1.68.0'
|
|
37
|
-
- '@oxlint/binding-linux-x64-musl@1.68.0'
|
|
38
|
-
- '@oxlint/binding-openharmony-arm64@1.68.0'
|
|
39
|
-
- '@oxlint/binding-win32-arm64-msvc@1.68.0'
|
|
40
|
-
- '@oxlint/binding-win32-ia32-msvc@1.68.0'
|
|
41
|
-
- '@oxlint/binding-win32-x64-msvc@1.68.0'
|
|
42
|
-
- oxfmt@0.53.0
|
|
43
|
-
- oxlint@1.68.0
|
|
4
|
+
- '@k03mad/oxlint-config@0.11.0'
|
|
5
|
+
- tsx@4.23.0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {describe, it} from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {ip2geo} from '../app/api.ts';
|
|
5
|
+
import type {ReqOutput} from '../app/types.ts';
|
|
6
|
+
|
|
7
|
+
import {getCurrentFilename, getTestFolder} from './helpers/path.ts';
|
|
8
|
+
import {REQUEST_IPV4} from './shared/consts.ts';
|
|
9
|
+
import {checkCacheFile, removeCacheFolder} from './shared/fs.ts';
|
|
10
|
+
|
|
11
|
+
const testName = getCurrentFilename(import.meta.url);
|
|
12
|
+
|
|
13
|
+
describe(testName, () => {
|
|
14
|
+
const opts = {
|
|
15
|
+
cacheDir: getTestFolder(testName),
|
|
16
|
+
cacheMap: new Map<string, ReqOutput>(),
|
|
17
|
+
cacheMapMaxEntries: 0,
|
|
18
|
+
tries: 5,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
22
|
+
|
|
23
|
+
Array.from({length: opts.tries}, (_, i) => i + 1).forEach(num => {
|
|
24
|
+
describe(`Try: ${num}`, () => {
|
|
25
|
+
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
26
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip ?? '', ...opts});
|
|
27
|
+
assert.deepEqual(data, REQUEST_IPV4);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should have cache file', () =>
|
|
31
|
+
checkCacheFile({
|
|
32
|
+
...opts,
|
|
33
|
+
response: REQUEST_IPV4,
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {describe, it} from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {ip2geo} from '../app/api.ts';
|
|
5
|
+
import type {ReqOutput} from '../app/types.ts';
|
|
6
|
+
|
|
7
|
+
import {getCurrentFilename, getTestFolder} from './helpers/path.ts';
|
|
8
|
+
import {REQUEST_IPV4} from './shared/consts.ts';
|
|
9
|
+
import {checkCacheFile, removeCacheFolder} from './shared/fs.ts';
|
|
10
|
+
|
|
11
|
+
const testName = getCurrentFilename(import.meta.url);
|
|
12
|
+
|
|
13
|
+
describe(testName, () => {
|
|
14
|
+
const opts = {
|
|
15
|
+
cacheDir: getTestFolder(testName),
|
|
16
|
+
cacheFileName: 'ips.md',
|
|
17
|
+
cacheFileSeparator: '-_-',
|
|
18
|
+
cacheFileNewline: '%%%',
|
|
19
|
+
cacheMap: new Map<string, ReqOutput>(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
23
|
+
|
|
24
|
+
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
25
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip ?? '', ...opts});
|
|
26
|
+
assert.deepEqual(data, REQUEST_IPV4);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have cache file', () =>
|
|
30
|
+
checkCacheFile({
|
|
31
|
+
...opts,
|
|
32
|
+
response: REQUEST_IPV4,
|
|
33
|
+
}));
|
|
34
|
+
});
|